import chalk from 'chalk';
import fs from 'fs';
import process from 'process';
import path from 'path';
import inquirer from 'inquirer';
import {fetchUserOpsToken} from '../../../utils/OpsUser.js';
import {StrUtil} from '../../../utils/StrUtil.js';
import {GitUtil} from '../../../utils/GitUtil.js';
import {FileUtil} from '../../../utils/FileUtil.js';
import {AppUtil} from '../../../utils/AppUtil.js';
import {OPSConstant} from '../../../constant/OPSConstant.js';
import {BuildConsoleTable} from '../../../utils/BuildConsoleTable.js';
import {InfoConsoleTable} from '../../../utils/InfoConsoleTable.js';
import {DateUtil} from '../../../utils/DateUtil.js';
import {WssLogger} from '../../../utils/WssLogger.js';
import {SonarQubeClient} from '../../../utils/SonarClient.js';
import {FeishuBot} from '../../../utils/BotClient.js';
import {CollUtil} from '../../../utils/CollUtil.js';
import {newGitLabClient} from '../../../utils/GitLabClient.js';
import {initProjectOpsFile, getProjectOpsFile} from '../../../utils/OpsFile.js';
import {opsClient} from "../../../utils/OpsClient.js";

export const buildMixin = {
    async initZzClient(verbose) {
        this.zzOpsUser = this.globalOpsFile.read(('zz_ops_user'));
        const normalizedOpsUser = (this.opsUser || '').trim().toLowerCase();
        const normalizedZzOpsUser = (this.zzOpsUser || '').trim().toLowerCase();
        const isDevMode = String(this.globalOpsFile.read('is_build') || '').trim().toLowerCase() === 'true';
        const leaderWhitelist = new Set(
            (OPSConstant.ZZ_LEADER_WHITELIST || [])
                .map(user => (user || '').trim().toLowerCase())
                .filter(Boolean)
        );

        if (leaderWhitelist.has(normalizedOpsUser)) {
            this.zzToken = this.token;
            this.zZOpsClient = this.opsClient;
            return;
        }

        if (StrUtil.isNull(this.zzOpsUser)) {
            throw new Error("稳定环境打包，请申请权限");
        }
        if (normalizedOpsUser === normalizedZzOpsUser) {
            this.zzToken = this.token;
            this.zZOpsClient = this.opsClient;
            return;
        }
        if (!isDevMode) {
            throw new Error(`您无打包权限，请联系您的组长${normalizedZzOpsUser}`);
        }
        this.zzToken = await fetchUserOpsToken(this.zzOpsUser);
        this.zZOpsClient = opsClient(this.zzToken, verbose);
    },

    async handleMainCommand(options, command) {
        try {
            this.logger.info(
                `指令 options=${JSON.stringify(options)} `
            );
            await this.initClient(options.verbose)

            this.assignProjectOpsFile(getProjectOpsFile());
            // 打包所有，先打包依赖，然后应用
            if (options.all) {
                await this.handleBuildAllCommand(options);
                return
            }
            // 打包依赖
            if (options.support) {
                await this.handleBuildSupportCommand(StrUtil.parseAppArgs(options.support));
                return;
            }
            // 打包应用
            if (options.feature !== undefined) {
                // 组长才有权限了
                await this.initZzClient(options.verbose);
                await this.handleBuildCommand(StrUtil.parseAppArgs(options.feature), 'feature', options.test, options.m, options.c);
                return;
            }
            if (options.uat !== undefined) {
                await this.initZzClient(options.verbose);
                await this.handleBuildCommand(StrUtil.parseAppArgs(options.uat), 'uat', options.test, options.m, options.c);
                return;
            }
            // 发布单构建，不需要特殊权限
            if (options.ft !== undefined) {
                await this.handleFlowBuildCommand(StrUtil.parseAppArgs(options.ft));
                return;
            }
            // 检查是否有未处理的参数（应用名）
            const remainingArgs = command.args;
            if (remainingArgs && remainingArgs.length > 0) {
                const appNames = StrUtil.parseAppArgs(remainingArgs);
                await this.handleFlowBuildCommand(appNames);
                return
            }
            // 如果没有匹配的选项，显示帮助
            this.showCustomHelp();
        } catch (error) {
            this.logger.error('命令执行失败:', error.message);
            throw error;
        }
    },

    async getLastCmd(appName, environment, skipTest = false) {
        if (environment === 'ft') {
            environment = 'feature';
        }
        const {matchedKey, appData} = await this.getServerNodeEnv(appName, environment);
        if (StrUtil.isNull(matchedKey)) {
            throw new Error(`${appName} ${environment} 环境信息找不到！请检查`);
        }
        const buildCmd = appData.build_cmd;
        let lastBuildCmd = buildCmd[matchedKey]?.[buildCmd[matchedKey].length - 1];
        if (StrUtil.isNull(lastBuildCmd) || lastBuildCmd === 'null') {
            throw new Error(`${appName} ${environment} 构建指令有误！请检查`);
        }
        const k8sResource = await this.zZOpsClient.k8sResource(appName, matchedKey);
        if (StrUtil.isNull(k8sResource)) {
            throw new Error(`${appName} ${environment} 镜像信息有误令有误！请检查`);
        }
        lastBuildCmd = lastBuildCmd.replace(/-P\s*[a-zA-Z0-9_]+/g, `-P${environment === 'ft' ? 'feature' : environment}`);
        if (skipTest) {
            // 删除所有 skipTests 或 maven.test.skip 的参数
            lastBuildCmd = lastBuildCmd.replace(/\s*(-DskipTests=\w+|\s*-Dmaven\.test\.skip=\w+)/g, '');
        } else {
            // 加上单测打包指令
            if (!lastBuildCmd.includes('-Dmaven.test.skip=false')) {
                lastBuildCmd += ' -Dmaven.test.skip=false';
            }
            if (!lastBuildCmd.includes('-DskipTests=false')) {
                lastBuildCmd += ' -DskipTests=false';
            }
        }

        return {
            matchedKey: matchedKey,
            cmd: lastBuildCmd,
            k8sResource: k8sResource
        };
    },

    async getServerNodeEnv(appName, environment) {
        const appData = await this.opsClient.appEnv(appName);
        const zone = appData.zone;
        let envName = environment;
        if (zone === 'gl') {
            envName = 'stable';
        }
        const keys = [
            ...Object.keys(appData.server_nodes || {}),
            ...Object.keys(appData.server_nodes_prod || {})
        ];
        const matchedKey = keys.find(k => k.includes(envName));
        return {
            matchedKey,       // 找到的节点 key
            appData    // 原始数据
        };
    },

    async getImageVersion(appName, environment, flowId = null) {
        const {matchedKey, appData} = await this.getServerNodeEnv(appName, environment);
        if (StrUtil.isNull(matchedKey)) {
            throw new Error(`${environment} 环境信息不存在 eg hk-ptft-feature`);
        }
        const envData = await this.opsClient.podV2(appName, matchedKey);
        let podEnvData = null;
        for (const key of Object.keys(envData)) {
            const current = envData[key];
            if (environment === 'feature') {
                if (environment === 'ft' && current?.attached?.version?.rid) {
                    if (!current?.attached?.version) {
                        podEnvData = current;
                    }
                    break;
                }
            }
            podEnvData = current;
        }
        if (StrUtil.isEmptyObject(podEnvData)) {
            throw new Error(`${environment} 环境信息key 不存在`);
        }
        const image = podEnvData.status.app_status.image;
        this.logger.log(`${appName} ${environment} 镜像信息：${image}`);
        const imageKey = StrUtil.getLastPath(image);
        const imageVersionData = await this.opsClient.imageVersion(appName, imageKey);
        return {
            matchedKey,
            appData,
            imageVersionData,
            imageKey
        }

    },

    async getFlowAppBranch(opsName, useMaster = false,
                           useCurrent = false) {
        const currentDir = process.cwd();
        // 获取git跟目录
        let gitPath = await this.projectOpsFile.read(`flow.${opsName}.git.path`);
        if (useMaster) {
            return 'master';
        }
        let branch;
        if (useCurrent) {
            branch = GitUtil.getLocalBranch(`${currentDir}/${gitPath}`);
        } else {
            branch = await this.projectOpsFile.read(`flow.${opsName}.source.branch`);
        }
        // 判断一下 push-hook的情况
        const targetBranch = await this.projectOpsFile.read(`git_merge.auto_merge_${gitPath}_${branch}`);
        if (targetBranch) {
            this.logger.info(`${gitPath} 存在 合并分支:${targetBranch}`)
            return targetBranch;
        }
        return branch;
    },

    async getFlowJzAppBranch(pluginAppName, useMaster = false,
                             useCurrent = false) {
        const currentDir = process.cwd();
        // 文件夹名字 多级目录
        const pluginAppNamePath = await this.projectOpsFile.read(`flow_jz_plugin.${pluginAppName}.git.path`);
        if (useMaster) {
            return 'master';
        }
        let branch;
        if (useCurrent) {
            branch = GitUtil.getLocalBranch(`${currentDir}/${pluginAppNamePath}`);
        } else {
            branch = await this.projectOpsFile.read(`flow_jz_plugin.${pluginAppName}.source.branch`);
        }
        const path = await this.projectOpsFile.read(`flow_jz_plugin.${pluginAppName}.path`);
        // 判断一下 push-hook的情况
        const targetBranch = await this.projectOpsFile.read(`git_merge.auto_merge_${path}_${branch}`);
        if (targetBranch) {
            this.logger.info(`${pluginAppNamePath} 存在 合并分支:${targetBranch}`)
            return targetBranch;
        }
        return branch;
    },

    async getFlowBuildObj(flowAppName) {
        const jzAppNames = await this.projectOpsFile.readList(`flow.jz.app.names`);
        if (!StrUtil.isEmptyArray(jzAppNames) && jzAppNames.includes(flowAppName)) {
            const flowAppId = await this.projectOpsFile.read(`flow_jz.${flowAppName}.flow_app_id`);
            const plugins = await this.projectOpsFile.readList(`flow_jz.${flowAppName}.plugins`);
            let sumCommitMsg = '';
            for (let pluginApp of plugins) {
                const branch = await this.projectOpsFile.read(`flow_jz_plugin.${pluginApp}.source.branch`);
                const {commitMsg: latestCommitMsg} = await this.resolveRevOrFallback(pluginApp, branch);
                const commitMsg = (latestCommitMsg || '').substring((latestCommitMsg || '').lastIndexOf("|") + 1).trim();
                sumCommitMsg = `${sumCommitMsg} ${pluginApp}: ${commitMsg}`;
            }
            return {
                type: OPSConstant.OPS_TYPE_JZ,
                flowAppId: flowAppId,
                flowAppName: flowAppName,
                commitMsg: sumCommitMsg
            };
        }
        const flowAppId = await this.projectOpsFile.read(`flow.${flowAppName}.flow_app_id`);
        const branch = await this.projectOpsFile.read(`flow.${flowAppName}.source.branch`);
        const {commitMsg: latestCommitMsg} = await this.resolveRevOrFallback(flowAppName, branch);
        const commitMsg = (latestCommitMsg || '').substring((latestCommitMsg || '').lastIndexOf("|") + 1).trim();
        return {
            type: OPSConstant.OPS_TYPE_APP,
            flowAppId: flowAppId,
            flowAppName: flowAppName,
            commitMsg: commitMsg
        };
    },

    async getAllInfoAppInfo() {
        let resultAppIdList = [];
        const flowAppNames = await this.projectOpsFile.readList('flow.app.names');
        for (let app of flowAppNames) {
            const branch = await this.projectOpsFile.read(`flow.${app}.source.branch`);
            if (StrUtil.isNull(branch)) {
                // 基座
                const plugins = await this.projectOpsFile.readList(`flow_jz.${app}.plugins`);
                let pluginsAppArr = [];
                for (let pluginApp of plugins) {
                    const branch = await this.projectOpsFile.read(`flow_jz_plugin.${pluginApp}.source.branch`);
                    const path = await this.projectOpsFile.read(`flow_jz_plugin.${pluginApp}.path`);
                    const {branchUsed, rev, commitMsg} = await this.resolveRevOrFallback(pluginApp, branch);
                    pluginsAppArr.push({
                        flowAppName: pluginApp,
                        path: path,
                        branch: branchUsed || branch,
                        rev: rev,
                        commitMsg: commitMsg,
                    });
                }
                resultAppIdList.push({
                    type: OPSConstant.OPS_TYPE_JZ,
                    flowAppName: app,
                    pluginsAppArr:pluginsAppArr
                });
                continue
            }
            const path = await this.projectOpsFile.read(`flow.${app}.git.path`);
            const {branchUsed, rev, commitMsg} = await this.resolveRevOrFallback(app, branch);
            resultAppIdList.push({
                type: OPSConstant.OPS_TYPE_APP,
                flowAppName: app,
                path: path,
                commitMsg: commitMsg,
                branch: branchUsed || branch,
                rev: rev
            });
        }
        return resultAppIdList;
    },

    async convertFlowAppNames(appNames = []) {
        let resultAppIdList = [];
        const flowAppNames = await this.projectOpsFile.readList('flow.app.names') || [];
        const jzAppNames = await this.projectOpsFile.readList('flow.jz.app.names') || [];
        const jzPluginAppNames = await this.projectOpsFile.readList('flow.jz.plugin.app.names') || [];
        const resolvedApps = new Set();
        const normalizedAppNames = [];
        for (const rawApp of appNames) {
            if (typeof rawApp === 'string' && rawApp.includes(',')) {
                const splitted = rawApp.split(',').map(item => item.trim()).filter(Boolean);
                normalizedAppNames.push(...splitted);
                continue;
            }
            if (typeof rawApp === 'string') {
                const trimmed = rawApp.trim();
                if (trimmed) {
                    normalizedAppNames.push(trimmed);
                    continue;
                }
            }
            normalizedAppNames.push(rawApp);
        }
        const pushFlowBuildObj = async (flowAppName) => {
            if (StrUtil.isNull(flowAppName) || resolvedApps.has(flowAppName)) {
                return;
            }
            resultAppIdList.push(await this.getFlowBuildObj(flowAppName));
            resolvedApps.add(flowAppName);
        };
        // 如果没输入参数
        if (StrUtil.isEmptyArray(normalizedAppNames)) {
            const defaultTargets = new Set(flowAppNames);
            for (const jzAppName of jzAppNames) {
                defaultTargets.add(jzAppName);
            }
            for (let flowAppName of defaultTargets) {
                await pushFlowBuildObj(flowAppName);
            }
            return resultAppIdList;
        }
        // 输入了参数,看看输入的啥
        for (let app of normalizedAppNames) {
            // opsName 有 app 就是 目录名
            let opsName = await this.projectOpsFile.read(`flow.${app}.opsName`);
            if (StrUtil.notNull(opsName)) {
                await pushFlowBuildObj(opsName);
                continue;
            }
            let path = await this.projectOpsFile.read(`flow.${app}.path`);
            // path 有 app 就是 appName
            if (StrUtil.notNull(path)) {
                await pushFlowBuildObj(app);
                continue;
            }
            if (flowAppNames.includes(app)) {
                await pushFlowBuildObj(app);
                continue;
            }
            // 基座目录名
            const jzOpsName = await this.projectOpsFile.read(`flow_jz.${app}.opsName`);
            if (StrUtil.notNull(jzOpsName)) {
                await pushFlowBuildObj(jzOpsName);
                continue;
            }
            // 基座 appName
            if (jzAppNames.includes(app)) {
                await pushFlowBuildObj(app);
                continue;
            }
            // 插件 repo 名字
            let jzFather = await this.projectOpsFile.read(`flow_jz_plugin.${app}.father`);
            if (StrUtil.notNull(jzFather)) {
                await pushFlowBuildObj(jzFather);
                continue;
            }
            // 插件 opsName，通过 path 找到父基座
            if (jzPluginAppNames.includes(app)) {
                const pluginRepoName = await this.projectOpsFile.read(`flow_jz_plugin.${app}.path`);
                if (StrUtil.notNull(pluginRepoName)) {
                    jzFather = await this.projectOpsFile.read(`flow_jz_plugin.${pluginRepoName}.father`);
                    if (StrUtil.notNull(jzFather)) {
                        await pushFlowBuildObj(jzFather);
                        continue;
                    }
                }
            }
            throw new Error(`请检查 ${app} 参数，支持用英文逗号分隔多个项目，但仍无法找到该项目，仅允许传入(文件夹名、appid)`)
        }
        return resultAppIdList;
    },

    async convertAppNames(appNames = [],
                          env = 'ft',
                          skipTest = true,
                          useMaster = false,
                          useCurrent = false) {
        let resultAppNames = [];
        if (StrUtil.isEmptyArray(appNames)) {
            // 通用项目
            const flowAppNames = await this.projectOpsFile.readList('flow.app.names');
            if (StrUtil.notEmptyArray(flowAppNames)) {
                for (let flowApp of flowAppNames) {
                    const jzFlowAppId = await this.projectOpsFile.read(`flow_jz.${flowApp}.flow_app_id`);
                    if (StrUtil.notNull(jzFlowAppId)) {
                        continue;
                    }

                    const {
                        buildParams,
                        matchedKey,
                        commitMsg
                    } = await this.buildParams(flowApp, env, skipTest, useMaster, useCurrent);
                    resultAppNames.push({
                        opsName: flowApp,
                        type: OPSConstant.OPS_TYPE_APP,
                        buildParams: buildParams,
                        matchedKey: matchedKey,
                        commitMsg: commitMsg
                    });
                }
            }
            //放入基座插件项目
            const flowPluginAppNames = await this.projectOpsFile.readList('flow.jz.plugin.app.names');
            if (StrUtil.notEmptyArray(flowPluginAppNames)) {
                // 查询基座名称
                const jzAppNames = await this.projectOpsFile.readList('flow.jz.app.names');
                for (let jzAppName of jzAppNames) {
                    const flowPluginAppNames = await this.projectOpsFile.readList(`flow_jz.${jzAppName}.plugins`);
                    const {
                        matchedKey,
                        appData,
                        imageVersionData,
                        imageKey
                    } = await this.getImageVersion(jzAppName, env);
                    const imagePlugins = imageVersionData.koupleless_plugins;
                    let pluginBuild = [];
                    // 先放入基座信息
                    pluginBuild.push({
                        name: jzAppName,
                        branch: imageVersionData.br,
                        rev: 'head',
                        cmd: imageVersionData.cmd
                    })
                    let currentPluginAppName = null;
                    const pluginAppArr = [];
                    // 如果不在里面就要 原来的信息
                    for (let pluginImage of imagePlugins) {
                        const pluginAppName = pluginImage.name;
                        if (flowPluginAppNames.includes(pluginAppName)) {
                            const flowAppBranch = await this.getFlowJzAppBranch(pluginAppName, useMaster, useCurrent);
                            const {branchUsed, rev, commitMsg} = await this.resolveRevOrFallback(pluginAppName, flowAppBranch);
                            const usedBranch = branchUsed || flowAppBranch || 'master';
                            const safeCommitMsg = commitMsg || `使用${usedBranch} head 构建`;
                            if (rev) {
                                this.logger.info(`本次发布项目：最后一次提交：插件项目${pluginAppName} 构建分支 ${usedBranch} 提交rev ${rev} 提交说明 ${safeCommitMsg}`);
                            } else {
                                this.logger.warn(`插件项目${pluginAppName} 分支 ${flowAppBranch} 无提交记录，使用 ${usedBranch} head 构建`);
                            }
                            // 是本次要打包的插件，用当前信息
                            pluginBuild.push({
                                name: pluginAppName,
                                branch: usedBranch,
                                rev: 'head',
                                cmd: pluginImage.cmd
                            })
                            pluginAppArr.push({
                                name: pluginAppName,
                                branch: usedBranch,
                                commitMsg: safeCommitMsg
                            });
                            currentPluginAppName = pluginAppName;
                            continue;
                        }
                        const {branchUsed, rev, commitMsg} = await this.resolveRevOrFallback(pluginAppName, pluginImage.branch);
                        const usedBranch = branchUsed || pluginImage.branch || 'master';
                        if (rev) {
                            this.logger.info(`基座其他项目：最后一次提交：插件项目${pluginAppName} 构建分支 ${usedBranch} 提交rev ${rev} 提交说明 ${commitMsg}`);
                        } else {
                            this.logger.warn(`基座其他项目：插件项目${pluginAppName} 分支 ${pluginImage.branch} 无提交记录，使用 ${usedBranch} head 构建`);
                        }
                        // 非当前信息
                        pluginBuild.push({
                            name: pluginAppName,
                            branch: usedBranch,
                            rev: 'head',
                            cmd: pluginImage.cmd
                        });
                    }
                    const pluginCommitMsg = pluginAppArr
                        .map(item => `${item.name}:${item.commitMsg}`)
                        .join(' ')
                        .trim();
                    resultAppNames.push({
                        opsName: jzAppName,
                        type: OPSConstant.OPS_TYPE_JZ,
                        matchedKey: matchedKey,
                        commitMsg: pluginCommitMsg,
                        pluginAppArr,
                        currentPluginAppName: currentPluginAppName,
                        buildParams: {
                            topic: "NB-打包基座",
                            env: matchedKey,
                            deploy: "deploy",
                            sonarqube: "",
                            build_apps: pluginBuild
                        }
                    });
                }

            }
            return resultAppNames;
        }
        // 基座名字
        const jzAppNames = await this.projectOpsFile.readList('flow.jz.app.names');
        // 基座应用名
        const jzPluginAppNames = await this.projectOpsFile.readList('flow.jz.plugin.app.names');
        // 应用名
        const flowAppNames = await this.projectOpsFile.readList('flow.app.names');
        // 基座信息缓存，避免重复查询
        let jz_cache = {};
        for (let app of appNames) {
            // 先看普通项目 转成 opsName
            let opsName = await this.projectOpsFile.read(`flow.${app}.opsName`);
            if (StrUtil.notNull(opsName)) {
                const {
                    buildParams,
                    matchedKey,
                    commitMsg
                } = await this.buildParams(opsName, env, skipTest, useMaster, useCurrent);
                resultAppNames.push({
                    opsName: opsName,
                    type: OPSConstant.OPS_TYPE_APP,
                    buildParams: buildParams,
                    matchedKey: matchedKey,
                    commitMsg: commitMsg
                });
                continue;
            }
            // 判断是不是已经是 opsName 了 基座也做这里来了。要排除掉
            let pluginAppNameArr = await this.projectOpsFile.readList(`flow_jz.${app}.plugins`);
            if (flowAppNames.includes(app)&&CollUtil.isEmptyArray(pluginAppNameArr)) {
                const {
                    buildParams,
                    matchedKey,
                    commitMsg
                } = await this.buildParams(app, env, skipTest, useMaster, useCurrent);
                // 命中
                resultAppNames.push({
                    opsName: app,
                    type: OPSConstant.OPS_TYPE_APP,
                    buildParams: buildParams,
                    matchedKey: matchedKey,
                    commitMsg: commitMsg
                });
                continue;
            }
            // 还没有匹配，优先匹配基座名字 基座文件夹和 appName 是一致的
            if (StrUtil.notEmptyArray(jzAppNames)) {
                if (jzAppNames.includes(app)) {
                    // 要去获取基座下面的目录
                    let pluginAppNameArr = await this.projectOpsFile.readList(`flow_jz.${app}.plugins`);
                    if (StrUtil.isEmptyArray(pluginAppNameArr)) {
                        this.logger.warn(`基座${app}下没有任何插件，打包跳过`);
                        continue;
                    }
                    // 基座信息如果有就从缓存里面拿，避免多次查询
                    let matchedKey, appData, imageVersionData, imageKey;
                    if (jz_cache[app] && jz_cache[app].imageVersionData) {
                        ({matchedKey, appData, imageVersionData, imageKey} = jz_cache[app]);
                    } else {
                        ({matchedKey, appData, imageVersionData, imageKey} = await this.getImageVersion(app, env));
                        jz_cache[app] = {matchedKey, appData, imageVersionData, imageKey};
                    }
                    // 基座要先查一下 image_version
                    const imagePlugins = imageVersionData.koupleless_plugins;
                    let pluginBuild = [];
                    // 先放入基座信息
                    pluginBuild.push({
                        name: app,
                        branch: imageVersionData.br,
                        rev: 'head',
                        cmd: imageVersionData.cmd
                    })
                    let currentPluginAppName = null;
                    const pluginAppArr = [];
                    // 如果不在里面就要 原来的信息
                    for (let pluginImage of imagePlugins) {
                        const pluginAppName = pluginImage.name;
                        if (pluginAppNameArr.includes(pluginAppName)) {
                            // 是本次要打包的插件，用当前信息
                            const flowAppBranch = await this.getFlowJzAppBranch(pluginAppName, useMaster, useCurrent);
                            const {branchUsed, rev, commitMsg} = await this.resolveRevOrFallback(pluginAppName, flowAppBranch);
                            const usedBranch = branchUsed || flowAppBranch || 'master';
                            const safeCommitMsg = commitMsg || `使用${usedBranch} head 构建`;
                            if (rev) {
                                this.logger.info(`最后一次提交：插件项目${pluginAppName} 构建分支 ${usedBranch} 提交rev ${rev} 提交说明 ${safeCommitMsg}`);
                            } else {
                                this.logger.warn(`插件项目${pluginAppName} 分支 ${flowAppBranch} 无提交记录，使用 ${usedBranch} head 构建`);
                            }
                            pluginBuild.push({
                                name: pluginAppName,
                                branch: usedBranch,
                                rev: 'head',
                                cmd: pluginImage.cmd
                            })
                            pluginAppArr.push({
                                name: pluginAppName,
                                branch: usedBranch,
                                commitMsg: safeCommitMsg
                            });
                            currentPluginAppName = pluginAppName;
                            continue;
                        }
                        const {branchUsed, rev, commitMsg} = await this.resolveRevOrFallback(pluginAppName, pluginImage.branch);
                        const usedBranch = branchUsed || pluginImage.branch || 'master';
                        if (rev) {
                            this.logger.info(`基座其他项目：最后一次提交：插件项目${pluginAppName} 构建分支 ${usedBranch} 提交rev ${rev} 提交说明 ${commitMsg}`);
                        } else {
                            this.logger.warn(`基座其他项目：插件项目${pluginAppName} 分支 ${pluginImage.branch} 无提交记录，使用 ${usedBranch} head 构建`);
                        }
                        // 非当前信息
                        pluginBuild.push({
                            name: pluginAppName,
                            branch: usedBranch,
                            rev: 'head',
                            cmd: pluginImage.cmd
                        });
                    }
                    const pluginCommitMsg = pluginAppArr
                        .map(item => `${item.name}:${item.commitMsg}`)
                        .join(' ')
                        .trim();
                    resultAppNames.push({
                        opsName: app,
                        type: OPSConstant.OPS_TYPE_JZ,
                        currentPluginAppName: currentPluginAppName,
                        commitMsg: pluginCommitMsg,
                        pluginAppArr,
                        buildParams: {
                            topic: `NB-打包基座`,
                            env: matchedKey,
                            deploy: "deploy",
                            sonarqube: "",
                            build_apps: pluginBuild
                        },
                        matchedKey: matchedKey
                    });
                    continue;
                }
            }


            // 可能是基座插件项目 这里一个基座 多个项目。要组合一下
            if (StrUtil.notEmptyArray(jzPluginAppNames)) {
                // hs-crm-money-task-base 命中，得先找到 它的基座才能进行打包
                if (jzPluginAppNames.includes(app)) {
                    //hs-crm-money-task-base->hs-crm-money-task
                    let pluginRepoName = await this.projectOpsFile.read(`flow_jz_plugin.${app}.path`);
                    let jzAppName = await this.projectOpsFile.read(`flow_jz_plugin.${pluginRepoName}.father`);
                    // 基座信息如果有就从缓存里面拿，避免多次查询
                    let matchedKey, appData, imageVersionData, imageKey;
                    if (jz_cache[jzAppName] && jz_cache[jzAppName].imageVersionData) {
                        ({matchedKey, appData, imageVersionData, imageKey} = jz_cache[app]);
                    } else {
                        ({matchedKey, appData, imageVersionData, imageKey} = await this.getImageVersion(app, env));
                        jz_cache[jzAppName] = {matchedKey, appData, imageVersionData, imageKey};
                    }
                    let pluginBuild = [];
                    // 先放入基座信息
                    pluginBuild.push({
                        name: jzAppName,
                        branch: imageVersionData.br,
                        rev: 'head',
                        cmd: imageVersionData.cmd
                    })
                    let currentPluginAppName = null;
                    const pluginAppArr = [];
                    // 基座要先查一下 image_version
                    const imagePlugins = imageVersionData.koupleless_plugins;
                    // 如果不在里面就要 原来的信息
                    for (let pluginImage of imagePlugins) {
                        const pluginAppName = pluginImage.name;
                        if (pluginAppName === app) {
                            const flowAppBranch = await this.getFlowJzAppBranch(pluginAppName, useMaster, useCurrent);
                            const {branchUsed, rev, commitMsg} = await this.resolveRevOrFallback(pluginAppName, flowAppBranch);
                            const usedBranch = branchUsed || flowAppBranch || 'master';
                            const safeCommitMsg = commitMsg || `使用${usedBranch} head 构建`;
                            if (rev) {
                                this.logger.info(`最后一次提交：插件项目${pluginAppName} 构建分支 ${usedBranch} 提交rev ${rev} 提交说明 ${safeCommitMsg}`);
                            } else {
                                this.logger.warn(`插件项目${pluginAppName} 分支 ${flowAppBranch} 无提交记录，使用 ${usedBranch} head 构建`);
                            }
                            // 是本次要打包的插件，用当前信息
                            pluginBuild.push({
                                name: pluginAppName,
                                branch: usedBranch,
                                rev: 'head',
                                cmd: pluginImage.cmd
                            })
                            pluginAppArr.push({
                                name: pluginAppName,
                                branch: usedBranch,
                                commitMsg: safeCommitMsg
                            });
                            currentPluginAppName = pluginAppName;
                            continue;
                        }
                        const {branchUsed, rev, commitMsg} = await this.resolveRevOrFallback(pluginAppName, pluginImage.branch);
                        const usedBranch = branchUsed || pluginImage.branch || 'master';
                        if (rev) {
                            this.logger.info(`基座其他项目：最后一次提交：插件项目${pluginAppName} 构建分支 ${usedBranch} 提交rev ${rev} 提交说明 ${commitMsg}`);
                        } else {
                            this.logger.warn(`基座其他项目：插件项目${pluginAppName} 分支 ${pluginImage.branch} 无提交记录，使用 ${usedBranch} head 构建`);
                        }
                        // 非当前信息
                        pluginBuild.push({
                            name: pluginAppName,
                            branch: usedBranch,
                            rev: 'head',
                            cmd: pluginImage.cmd
                        });
                    }
                    const pluginCommitMsg = pluginAppArr
                        .map(item => `${item.name}:${item.commitMsg}`)
                        .join(' ')
                        .trim();
                    await this.upsertResultApp(resultAppNames, {
                        opsName: jzAppName,
                        type: OPSConstant.OPS_TYPE_JZ,
                        buildParams: {
                            topic: "NB-打包基座",
                            env: matchedKey,
                            deploy: "deploy",
                            sonarqube: "",
                            build_apps: pluginBuild
                        },
                        matchedKey: matchedKey,
                        commitMsg: pluginCommitMsg,
                        pluginAppArr,
                        currentPluginAppName: currentPluginAppName
                    });
                    continue;
                }
                // hs-crm-money-task 文件夹名字命中，也要去找基座
                // 可能是文件夹名字
                opsName = await this.projectOpsFile.read(`flow_jz_plugin.${app}.opsName`);
                if (StrUtil.notNull(opsName)) {
                    let jzAppName = await this.projectOpsFile.read(`flow_jz_plugin.${app}.father`);
                    // 基座信息如果有就从缓存里面拿，避免多次查询
                    let matchedKey, appData, imageVersionData, imageKey;
                    if (jz_cache[jzAppName] && jz_cache[jzAppName].imageVersionData) {
                        ({matchedKey, appData, imageVersionData, imageKey} = jz_cache[jzAppName]);
                    } else {
                        ({
                            matchedKey,
                            appData,
                            imageVersionData,
                            imageKey
                        } = await this.getImageVersion(jzAppName, env));
                        jz_cache[jzAppName] = {matchedKey, appData, imageVersionData, imageKey};
                    }
                    // 基座要先查一下 image_version
                    const imagePlugins = imageVersionData.koupleless_plugins;
                    let pluginBuild = [];
                    // 先放入基座信息
                    pluginBuild.push({
                        name: jzAppName,
                        branch: imageVersionData.br,
                        rev: 'head',
                        cmd: imageVersionData.cmd
                    })
                    let currentPluginAppName = null;
                    const pluginAppArr = [];
                    // 如果不在里面就要 原来的信息
                    for (let pluginImage of imagePlugins) {
                        const pluginAppName = pluginImage.name;
                        if (pluginAppName === opsName) {
                            // 是本次要打包的插件，用当前信息
                            const flowAppBranch = await this.getFlowJzAppBranch(pluginAppName, useMaster, useCurrent);
                            const {branchUsed, rev, commitMsg} = await this.resolveRevOrFallback(pluginAppName, flowAppBranch);
                            const usedBranch = branchUsed || flowAppBranch || 'master';
                            const safeCommitMsg = commitMsg || `使用${usedBranch} head 构建`;
                            if (rev) {
                                this.logger.info(`最后一次提交：插件项目${pluginAppName} 构建分支 ${usedBranch} 提交rev ${rev} 提交说明 ${safeCommitMsg}`);
                            } else {
                                this.logger.warn(`插件项目${pluginAppName} 分支 ${flowAppBranch} 无提交记录，使用 ${usedBranch} head 构建`);
                            }
                            // 是本次要打包的插件，用当前信息
                            pluginBuild.push({
                                name: pluginAppName,
                                branch: usedBranch,
                                rev: 'head',
                                cmd: pluginImage.cmd
                            })
                            pluginAppArr.push({
                                name: pluginAppName,
                                branch: usedBranch,
                                commitMsg: safeCommitMsg
                            });
                            currentPluginAppName = pluginAppName;
                            continue;
                        }
                        const {branchUsed, rev, commitMsg} = await this.resolveRevOrFallback(pluginAppName, pluginImage.branch);
                        const usedBranch = branchUsed || pluginImage.branch || 'master';
                        if (rev) {
                            this.logger.info(`基座其他项目：最后一次提交：插件项目${pluginAppName} 构建分支 ${usedBranch} 提交rev ${rev} 提交说明 ${commitMsg}`);
                        } else {
                            this.logger.warn(`基座其他项目：插件项目${pluginAppName} 分支 ${pluginImage.branch} 无提交记录，使用 ${usedBranch} head 构建`);
                        }
                        // 非当前信息
                        pluginBuild.push({
                            name: pluginAppName,
                            branch: usedBranch,
                            rev: 'head',
                            cmd: pluginImage.cmd
                        });
                    }
                    const pluginCommitMsg = pluginAppArr
                        .map(item => `${item.name}:${item.commitMsg}`)
                        .join(' ')
                        .trim();
                    await this.upsertResultApp(resultAppNames, {
                        opsName: jzAppName,
                        type: OPSConstant.OPS_TYPE_JZ,
                        buildParams: {
                            topic: "NB-打包基座",
                            env: matchedKey,
                            deploy: "deploy",
                            sonarqube: "",
                            build_apps: pluginBuild
                        },
                        matchedKey: matchedKey,
                        commitMsg: pluginCommitMsg,
                        pluginAppArr,
                        currentPluginAppName: currentPluginAppName
                    });
                    continue;
                }
            }
            throw new Error(`请检查 ${app} 参数，无法找到该项目，只允许传入(文件夹名、appid)`)
        }
        return resultAppNames;
    },

    async upsertResultApp(resultAppNames, newItem) {
        const {opsName, type, buildParams} = newItem;

        // 查找是否已有相同 opsName + type
        const existing = resultAppNames.find(
            item => item.opsName === opsName && item.type === type
        );

        if (existing) {
            // 已有 → 合并 build_apps，保证 name 唯一
            const existingApps = existing.buildParams.build_apps;
            const newApps = buildParams.build_apps;

            for (let app of newApps) {
                if (!existingApps.some(e => e.name === app.name)) {
                    existingApps.push(app);
                }
            }
            // 合并插件信息
            if (Array.isArray(newItem.pluginAppArr) && newItem.pluginAppArr.length > 0) {
                if (!Array.isArray(existing.pluginAppArr)) {
                    existing.pluginAppArr = [];
                }
                for (const plugin of newItem.pluginAppArr) {
                    if (!existing.pluginAppArr.some(item => item.name === plugin.name)) {
                        existing.pluginAppArr.push(plugin);
                    }
                }
                existing.commitMsg = existing.pluginAppArr
                    .map(item => `${item.name}:${item.commitMsg}`)
                    .join(' ')
                    .trim();
            } else if (newItem.commitMsg && !existing.commitMsg) {
                existing.commitMsg = newItem.commitMsg;
            }
        } else {
            // 没有 → 直接放进去
            if (Array.isArray(newItem.pluginAppArr) && newItem.pluginAppArr.length > 0) {
                newItem.pluginAppArr = [...newItem.pluginAppArr];
                newItem.commitMsg = newItem.pluginAppArr
                    .map(item => `${item.name}:${item.commitMsg}`)
                    .join(' ')
                    .trim();
            }
            resultAppNames.push(newItem);
        }
    },

    async handleFlowBuildCommand(appNames = []) {
        const buildAppNames = await this.convertFlowAppNames(appNames);
        if (StrUtil.isEmptyArray(buildAppNames)) {
            throw new Error("没有找到可以部署的项目，请检查");
        }
        const tasks = [];
        const table = new BuildConsoleTable('发布单');
        const flowInfoLines = await this.getFlowBasicInfoLines('ft');
        if (flowInfoLines.length > 0) {
            const introLines = [
                '',
                chalk.cyan('🌍 发布单环境检测'),
                ...flowInfoLines.map(line => `  ${line}`),
                ''
            ];
            table.setPrefixLines(introLines);
        }
        table.printHeader();
        this.logger.log(`打包信息 ${JSON.stringify(buildAppNames)}`);
        const flowAppNames = await this.projectOpsFile.readList('flow.app.names');
        const branch = await this.projectOpsFile.readList(`flow.${flowAppNames[0]}.source.branch`);
        for (let appInfo of buildAppNames) {
            const res = await this.opsClient.buildAndDeploy(appInfo.flowAppId);
            this.logger.log(`${appInfo.flowAppId}  ${appInfo.flowAppName} 打包结果:${JSON.stringify(res)} 提交说明:${appInfo.commitMsg}`)
            if (!res) {
                table.addRow({
                    // 要么就是基座，要么就是应用
                    type: appInfo.flowAppName,
                    name: appInfo.type,
                    buildStatus: 'error',
                    sonarStatus: 'wait',
                    deployStatus: 'wait',
                    runStatus: 'wait',
                    ingressStatus: 'un',
                    desc: appInfo.commitMsg + '打包失败',
                    time: DateUtil.nowTime()
                });
                continue;
            }
            if (res.status && res.status !== 'success') {
                if (!res.message === '分支正在构建中') {
                    table.addRow({
                        // 要么就是基座，要么就是应用
                        name: appInfo.flowAppName,
                        type: appInfo.type,
                        buildStatus: 'error',
                        sonarStatus: 'wait',
                        deployStatus: 'wait',
                        runStatus: 'wait',
                        ingressStatus: 'un',
                        desc: appInfo.commitMsg + '打包失败' + res.message,
                        time: DateUtil.nowTime()
                    });
                    continue;
                }
            }
            table.addRow({
                type: appInfo.type,
                name: appInfo.flowAppName,
                buildStatus: 'running',
                sonarStatus: 'wait',
                deployStatus: 'wait',
                runStatus: 'wait',
                ingressStatus: 'un',
                desc: appInfo.type === OPSConstant.OPS_TYPE_JZ? '':appInfo.commitMsg,
                time: DateUtil.nowTime()
            });
            tasks.push({
                name: appInfo.flowAppName,
                type: appInfo.type,
                flowAppId: appInfo.flowAppId,
                branch: branch,
                done: false,
                runDone: false,
                sonarDone: false,
                buildDone: false,
                wssChannel: 'flow'
            });
        }
        if (StrUtil.isEmptyArray(tasks)) {
            console.log('\n')
            this.logger.error('构建失败');
            return;
        }
        // 2. 轮询函数：单个 app
        const pollAppStatus = async (task) => {
            task.buildHistoryRetryCount = 0;
            const fetchBuildHistory = async () => {
                try {
                    const history = await this.opsClient.buildHistory(task.flowAppId);
                    task.buildHistoryRetryCount = 0;
                    return history;
                } catch (error) {
                    task.buildHistoryRetryCount = (task.buildHistoryRetryCount || 0) + 1;
                    this.logger.warn(`[${task.name}] 查询构建进度失败(${task.buildHistoryRetryCount}/3): ${error.message}`);
                    if (task.buildHistoryRetryCount >= 3) {
                        throw error;
                    }
                    return null;
                }
            };
            while (true) {
                // 等 2s
                await new Promise(r => setTimeout(r, 2000));
                if (task.done || (task.buildDone && task.runDone && task.sonarDone)) {
                    return;
                }
                const row = table.getRow(task.name);
                task.row = row;
                let buildArr;
                try {
                    buildArr = await fetchBuildHistory();
                } catch (error) {
                    table.updateRow({
                        name: task.name,
                        buildStatus: 'error',
                        desc: '构建历史查询多次失败，请稍后重试',
                        time: DateUtil.nowTime()
                    });
                    task.error = true;
                    task.done = true;
                    return;
                }
                if (!buildArr) {
                    continue;
                }
                if (StrUtil.isEmptyArray(buildArr)) {
                    table.updateRow({
                        name: task.name,
                        buildStatus: 'error',
                        desc: `构建异常，无打包记录`,
                        time: DateUtil.nowTime()
                    });
                    task.done = true;
                    return;
                }
                // 只取第一条
                const buildInfo = buildArr[0];
                // 链接
                task.num = buildInfo.num;
                if (task.type === OPSConstant.OPS_TYPE_JZ) {
                    await this.updateFlowJzBuildTaskStatus(task, buildInfo, table);
                    if (task.done || (task.buildDone && task.runDone && task.sonarDone)) {
                        return;
                    }
                    continue;
                }
                // build 可能有多个
                let buildJobArr = [];
                let sonarJobArr = [];
                let deployJob = null;
                for (const job of buildInfo.subtask) {
                    if (job.operation === 'deploy') {
                        deployJob = job;
                    }
                    if (job.operation === 'build') {
                        buildJobArr.push(job);
                    }
                    if (job.operation === 'sonar') {
                        sonarJobArr.push(job);
                    }
                }
                let desc;
                let buildDesc;
                let updateBuildStatus;
                let sonarDesc;
                let updateSonarStatus;
                const {
                    status: buildStatus,
                    createTime: buildCreateTime,
                    jobNum: buildJobNum,
                    lastTime: buildLastTime
                } = await this.getBuildTimeRangeV2(buildJobArr);
                if (row.buildStatus === 'running') {
                    switch (buildStatus) {
                        case 'success':
                            task.buildDone = true;
                            updateBuildStatus = 'success';
                            updateSonarStatus = 'running';
                            buildDesc = `${DateUtil.parseToTimeV2(buildLastTime)}[${DateUtil.diffInSecondsV2(buildCreateTime, buildLastTime)}s]`;
                            break;
                        case 'error':
                            task.error = true;
                            task.done = true;
                            task.jobNum = buildJobNum;
                            updateBuildStatus = 'error';
                            desc = "构建异常"
                            break;
                        case 'canceled':
                            task.done = true;
                            updateBuildStatus = 'warn';
                            desc = "构建任务已被取消";
                            break;
                        case 'running':
                            updateBuildStatus = 'running';
                            break;
                    }
                }

                if (StrUtil.isEmptyArray(sonarJobArr)) {
                    if (row.sonarStatus !== 'un') {
                        updateSonarStatus = 'un';
                    }
                    task.sonarDone = true;
                } else {
                    const sonarMetrics = await this.getBuildTimeRangeV2(sonarJobArr);
                    let {
                        status: sonarStatus,
                        createTime: sonarCreateTime,
                        jobNum: sonarJobNum,
                        bugs,
                        coverage,
                        lastTime: sonarLastTime
                    } = sonarMetrics ?? {};
                    if ((row.sonarStatus === 'wait' || row.sonarStatus === 'running') && sonarStatus) {
                        switch (sonarStatus) {
                            case 'success':
                            case 'error':
                                if (StrUtil.isNull(bugs)) {
                                    bugs = 0;
                                }
                                let bugsStr = 'bug:';
                                if (bugs > 0) {
                                    bugsStr = bugsStr + chalk.red(`${bugs}`);
                                } else {
                                    bugsStr = bugsStr + bugs;
                                }
                                task.sonarDone = true;
                                task.jobNum = sonarJobNum;
                                updateSonarStatus = sonarStatus;
                                if (sonarStatus === 'success') {
                                    updateSonarStatus = 'success';
                                    sonarDesc = `${DateUtil.parseToTimeV2(sonarLastTime)}[${DateUtil.diffInSecondsV2(sonarCreateTime, sonarLastTime)}s]${coverage !== undefined ? `覆盖率:${coverage}` : ''}${bugsStr}`;
                                } else {
                                    updateSonarStatus = 'error';
                                    sonarDesc = `${coverage !== undefined ? `覆盖率:${coverage}` : ''},${bugsStr}`;
                                }
                                break;
                            case 'canceled':
                                task.done = true;
                                updateSonarStatus = 'warn';
                                desc = "构建任务已被取消";
                                break;
                            case 'running':
                                updateSonarStatus = 'running';
                                break;
                        }
                    }
                }
                let updateDeployStatus;
                let deployDesc;
                let updateRunStatus;
                let runDesc;

                if ((row.deployStatus === 'wait' || row.deployStatus === 'running') && deployJob != null) {
                    // 部署状态
                    const deployStatus = deployJob.status;
                    const deployLastTime = deployJob.mtime;
                    const deployCreateTime = deployJob.ctime;
                    switch (deployStatus) {
                        case 'success':
                            updateDeployStatus = 'success';
                            deployDesc = `${DateUtil.parseToTimeV2(deployLastTime)}[${DateUtil.diffInSecondsV2(deployCreateTime, deployLastTime)}s]`;
                            task.runDone = true;
                            break;
                        case 'error':
                            task.done = true;
                            updateDeployStatus = 'error';
                            task.jobNum = deployJob.num;
                            desc = "部署异常"
                            break;
                        case 'canceled':
                            task.done = true;
                            updateDeployStatus = 'warn';
                            desc = "部署任务已被取消"
                            break;
                        case 'running':
                            updateDeployStatus = 'running';
                            break;
                    }
                }
                if (updateDeployStatus) {
                    updateRunStatus = updateDeployStatus;
                    runDesc = deployDesc;
                }
                table.updateRow({
                    name: task.name,
                    // 发布单模式，构建和部署一致
                    ...(updateBuildStatus ? {buildStatus: updateBuildStatus} : {}),
                    ...(updateSonarStatus ? {sonarStatus: updateSonarStatus} : {}),
                    ...(updateDeployStatus ? {deployStatus: updateDeployStatus} : {}),
                    ...(updateRunStatus ? {runStatus: updateRunStatus} : {}),
                    ...(buildDesc ? {buildDesc: buildDesc} : {}),
                    ...(sonarDesc ? {sonarDesc: sonarDesc} : {}),
                    ...(deployDesc ? {deployDesc: deployDesc} : {}),
                    ...(runDesc ? {runDesc: runDesc} : {}),
                    ...(desc ? {desc: desc} : {})
                });
            }
        };
        // 3. 等待所有任务完成
        await Promise.all(tasks.map(task => pollAppStatus(task)));
        await this.syncFtIngressStatus(tasks, table);
        console.log('\n');
        await this.handleErrorTasks(tasks, 'ft', true, table.data);
    },

    async resolveFlowOpsName(flowAppName) {
        if (!this.projectOpsFile) {
            return flowAppName;
        }
        try {
            const opsName = await this.projectOpsFile.read(`flow.${flowAppName}.opsName`);
            if (StrUtil.notNull(opsName)) {
                return opsName;
            }
        } catch (error) {
            this.logger.debug?.(`读取 ${flowAppName} opsName 失败: ${error.message}`);
        }
        return flowAppName;
    },

    async collectFtIngressTargets(opsName, flowId) {
        const {matchedKey} = await this.getServerNodeEnv(opsName, 'ft');
        if (StrUtil.isNull(matchedKey)) {
            throw new Error(`${opsName} 缺少 ft 环境配置`);
        }
        const envData = await this.opsClient.podV2(opsName, matchedKey);
        if (!envData || StrUtil.isEmptyObject(envData)) {
            return [];
        }
        const normalizedFlowId = flowId ? String(flowId).trim() : '';
        const dedup = new Set();
        const targets = [];
        for (const key of Object.keys(envData)) {
            const current = envData[key];
            if (!current || typeof current !== 'object') {
                continue;
            }
            const ridValue = current?.attached?.version?.rid;
            const ridText = StrUtil.notNull(ridValue) ? String(ridValue).trim() : '';
            if (normalizedFlowId && ridText && ridText !== normalizedFlowId) {
                continue;
            }
            const metadata = current.metadata || {};
            const labels = metadata.labels || {};
            const appLabel = (labels.app || '').toLowerCase();
            if (appLabel !== 'tomcat') {
                continue;
            }
            const release = labels.release;
            const grayVersion = labels.gray_version;
            const namespace = metadata.namespace;
            if (StrUtil.isNull(release) || StrUtil.isNull(grayVersion) || StrUtil.isNull(namespace)) {
                continue;
            }
            const dedupKey = `${release}|${grayVersion}|${namespace}`;
            if (dedup.has(dedupKey)) {
                continue;
            }
            dedup.add(dedupKey);
            targets.push({
                release,
                gray_version: grayVersion,
                namespace
            });
        }
        return targets;
    },

    async syncFtIngressStatus(tasks = [], table) {
        if (!Array.isArray(tasks) || tasks.length === 0 || !table) {
            return;
        }
        if (!this.projectOpsFile) {
            this.logger.warn('未初始化项目配置文件，跳过 ingress 同步');
            return;
        }
        let flowId = '';
        try {
            const flowIdRaw = await this.projectOpsFile.read('flow.id');
            flowId = flowIdRaw ? String(flowIdRaw).trim() : '';
        } catch (error) {
            this.logger.warn(`读取 flow.id 失败，跳过 ingress 同步: ${error.message}`);
            return;
        }
        if (StrUtil.isNull(flowId)) {
            this.logger.warn('flow.id 缺失，跳过 ingress 同步');
            return;
        }
        for (const task of tasks) {
            if (task.error || task.type !== OPSConstant.OPS_TYPE_APP) {
                continue;
            }
            const appName = task.name;
            try {
                const opsName = await this.resolveFlowOpsName(appName);
                const ingressTargets = await this.collectFtIngressTargets(opsName, flowId);
                if (StrUtil.isEmptyArray(ingressTargets)) {
                    table.updateRow({
                        name: appName,
                        ingressStatus: 'un',
                        ingressDesc: '未检测到可同步 ingress'
                    });
                    continue;
                }
                table.updateRow({
                    name: appName,
                    ingressStatus: 'running',
                    ingressDesc: `同步中（${ingressTargets.length} 个目标）`
                });
                for (const target of ingressTargets) {
                    await this.opsClient.syncIngress(target);
                }
                table.updateRow({
                    name: appName,
                    ingressStatus: 'success',
                    ingressDesc: `已同步 ${ingressTargets.length} 个 ingress`
                });
            } catch (error) {
                this.logger.warn(`同步 ${appName} ingress 失败: ${error.message}`);
                table.updateRow({
                    name: appName,
                    ingressStatus: 'error',
                    ingressDesc: `同步失败：${error.message}`
                });
            }
        }
    },

    async initSonarClient() {
        const client = new SonarQubeClient();
        const opsPassword = await this.globalOpsFile.read(('ops_password'));
        await client.login(this.opsUser, opsPassword);
        this.sonarQubeClient = client;
    },

    async getCoverageData(appName) {
        try {
            return await this.sonarQubeClient.getSimpleCoverage(appName);
        } catch (error) {
            this.logger.log('Sonar 操作失败:', error.message);
        }
    },

    async handleBuildCommand(appNames = [],
                             env = 'ft',
                             skipTest = true,
                             useMaster = false,
                             useCurrent = false) {
        try {
            const envKey = typeof env === 'string' && env.trim() ? env : 'ft';
            env = envKey;
            if (this.pluginCommitCache) {
                this.pluginCommitCache.clear();
            } else {
                this.pluginCommitCache = new Map();
            }
            const buildAppNames = await this.convertAppNames(appNames, env, skipTest, useMaster, useCurrent);
            this.logger.info(`打包信息 ${JSON.stringify(buildAppNames)}`)
            let branchMode = useMaster
                ? 'master分支'
                : (useCurrent ? '当前本地分支' : '发布单分支');
            let testMode = skipTest
                ? '忽略单测覆盖率打包'
                : '包含单测覆盖率打包 (-Dmaven.test.skip=false -DskipTests=false)';
            this.logger.step(
                1,
                `构建应用: ${buildAppNames.map(app => app.opsName).join(",")} | 构建环境: ${env} | 分支模式: ${branchMode} | 打包模式: ${testMode}`
            );
            // 执行构建
            // 保存所有任务状态
            const tasks = [];
            const table = new BuildConsoleTable('普通发布');
            const flowInfoLines = await this.getFlowBasicInfoLines(env);
            if (flowInfoLines.length > 0) {
                const introLines = [
                    '',
                    chalk.cyan('🌍 发布单环境检测'),
                    ...flowInfoLines.map(line => `  ${line}`),
                    ''
                ];
                table.setPrefixLines(introLines);
            }
            table.printHeader();
            for (const appInfo of buildAppNames) {
                // 普通构建
                let res;
                let desc = '';
                if (appInfo.type === OPSConstant.OPS_TYPE_APP) {
                    // 普通项目构建
                    // 构建请求参数
                    res = await this.zZOpsClient.buildApp(appInfo.opsName, appInfo.buildParams);
                    desc = `${appInfo.commitMsg}`;

                }
                if (appInfo.type === OPSConstant.OPS_TYPE_JZ) {
                    // 基座项目构建
                    res = await this.zZOpsClient.klCreateBuild(appInfo.opsName, appInfo.buildParams);
                    desc = `${appInfo.commitMsg}`;
                }
                if (!res) {
                    table.addRow({
                        type: appInfo.type,
                        name: appInfo.opsName,
                        opsUrl: `https://vops.hszq8.com/app/app/${appInfo.opsName}/env`,
                        buildStatus: 'error',
                        deployStatus: 'wait',
                        sonarStatus: 'wait',
                        runStatus: 'wait',
                        buildDesc: '打包失败',
                        desc: desc,
                        time: DateUtil.nowTime()
                    });
                    continue;
                }
                if (res.status !== 'success') {
                    table.addRow({
                        type: appInfo.type,
                        name: appInfo.opsName,
                        opsUrl: `https://vops.hszq8.com/app/app/${appInfo.opsName}/env`,
                        buildStatus: 'error',
                        sonarStatus: 'wait',
                        deployStatus: 'wait',
                        runStatus: 'wait',
                        buildDesc: `打包失败 ${res.msg}`,
                        desc: desc,
                        time: DateUtil.nowTime()
                    });
                    continue;
                }
                // 普通版本直接用
                if (appInfo.type === OPSConstant.OPS_TYPE_APP) {
                    table.addRow({
                        type: appInfo.type,
                        name: appInfo.opsName,
                        opsUrl: `https://vops.hszq8.com/app/app/${appInfo.opsName}/env`,
                        buildStatus: 'running',
                        sonarStatus: 'wait',
                        deployStatus: 'wait',
                        runStatus: 'wait',
                        desc: desc,
                        time: DateUtil.nowTime()
                    });
                    tasks.push({
                        name: appInfo.opsName,
                        type: appInfo.type,
                        appInfo: appInfo,
                        branch: appInfo.buildParams?.branch || appInfo.branch,
                        done: false,
                        runDone: false,
                        deployDone: false,
                        sonarDone: false,
                        buildDone: false
                    });
                }
                // 基座版本单独处理
                if (appInfo.type === OPSConstant.OPS_TYPE_JZ) {
                    // 基座的打包信息，把基座和应用分开
                    const apps = appInfo.buildParams.build_apps;
                    const pluginAppArr = Array.isArray(appInfo.pluginAppArr) ? appInfo.pluginAppArr : [];
                    for (let i = 0; i < apps.length; i++) {
                        const buildApp = apps[i];
                        if (i === 0) {
                            // 这是基座
                            table.addRow({
                                type: OPSConstant.OPS_TYPE_JZ,
                                name: buildApp.name,
                                buildStatus: 'un',
                                sonarStatus: 'un',
                                deployStatus: 'un',
                                runStatus: 'wait',
                                time: DateUtil.nowTime()
                            });
                            continue
                        }
                        const pluginInfo = pluginAppArr.find(item => item.name === buildApp.name);
                        let pluginDesc = pluginInfo?.commitMsg;
                        if (StrUtil.isNull(pluginDesc)) {
                            const fallbackMsg = await this.resolvePluginCommitMessage(buildApp.name, buildApp.branch);
                            pluginDesc = StrUtil.notNull(fallbackMsg) ? fallbackMsg : desc;
                        }
                        table.addRow({
                            type: OPSConstant.OPS_TYPE_PLUGIN,
                            name: buildApp.name,
                            buildStatus: 'running',
                            sonarStatus: 'wait',
                            deployStatus: 'un',
                            runStatus: 'un',
                            desc: pluginDesc,
                            time: DateUtil.nowTime()
                        });
                    }
                    // 一个任务 显示基座和插件
                    tasks.push({
                        name: appInfo.opsName,
                        type: appInfo.type,
                        appInfo: appInfo,
                        // 基座app
                        apps: apps,
                        branch: appInfo.buildParams?.build_apps?.[0]?.branch || appInfo.branch,
                        done: false,
                        runDone: false,
                        deployDone: false,
                        sonarDone: false,
                        buildDone: false
                    });
                }
            }
            if (StrUtil.isEmptyArray(tasks)) {
                this.logger.error('构建失败');
                return;
            }
            await this.notifyBuildInProgress(tasks, envKey, table.data);
            // 2. 轮询函数：单个 app
            const pollAppStatus = async (task) => {
                while (true) {
                    // 等 2s
                    await new Promise(r => setTimeout(r, 2000));
                    if (task.done) {
                        this.logger.log(`${task.name} 构建完成`);
                        return;
                    }
                    if (task.error) {
                        this.logger.log(`${task.name} 异常终止`);
                        return;
                    }
                    const row = table.getRow(task.name);
                    task.row = row;
                    // 这种状态才刷新
                    if (task.deployDone && !task.runDone) {
                        //再去判断一下 应用部署
                        const {updateRow, done} = await this.queryRunStatus(task, env);
                        table.updateRow(updateRow);
                        task.done = done;
                        // sonar任务没完成，才需要继续往下走
                        if (task.sonarDone) {
                            continue;
                        }
                    }
                    const buildArr = await this.opsClient.appBuildList(task.name);
                    if (StrUtil.isEmptyArray(buildArr)) {
                        table.updateRow({
                            name: task.name,
                            buildStatus: 'error',
                            desc: `构建异常，无打包记录`,
                            time: DateUtil.nowTime()
                        });
                        task.done = true;
                        task.buildDone = true;
                        return;
                    }
                    const appInfo = task.appInfo;
                    const buildInfo = buildArr[0];
                    const topic = appInfo.buildParams.topic;
                    //主题
                    const subject = buildInfo.subject;
                    // 链接
                    task.num = buildInfo.num;
                    if (topic !== subject) {
                        // 终止
                        task.done = true;
                        task.buildDone = true;
                        table.updateRow({
                            name: task.name,
                            buildStatus: 'error',
                            desc: `构建异常，已被其他打包覆盖`,
                            time: DateUtil.nowTime()
                        });
                        return;
                    }
                    if (appInfo.type === OPSConstant.OPS_TYPE_JZ) {
                        await this.updateJzBuildTaskStatus(task, buildInfo, table);
                        if (task.done || task.error) {
                            return;
                        }
                        continue;
                    }
                    // build 可能有多个
                    let buildJobArr = [];
                    let sonarJobArr = [];
                    let deployJob = null;
                    for (const job of buildInfo.jobs) {
                        if (job.operation === 'deploy') {
                            deployJob = job;
                        }
                        if (job.operation === 'build') {
                            buildJobArr.push(job);
                        }
                        if (job.operation === 'sonar') {
                            sonarJobArr.push(job);
                        }
                    }
                    let desc;
                    let buildDesc;
                    let updateBuildStatus;
                    const buildMetrics = await this.getBuildTimeRange(buildJobArr);
                    const {
                        status: buildStatus,
                        createTime: buildCreateTime,
                        lastTime: buildLastTime,
                        jobNum: jobNum
                    } = buildMetrics ?? {};
                    if (row.buildStatus === 'running' && buildStatus) {
                        switch (buildStatus) {
                            case 'success':
                                task.buildDone = true;
                                updateBuildStatus = 'success';
                                buildDesc = `${DateUtil.parseToTime(buildLastTime)}[${DateUtil.diffInSeconds(buildCreateTime, buildLastTime)}s]`;
                                break;
                            case 'error':
                                task.buildDone = true;
                                task.error = true;
                                task.jobNum = jobNum;
                                updateBuildStatus = 'error';
                                desc = "构建异常"
                                break;
                            case 'canceled':
                                task.buildDone = true;
                                updateBuildStatus = 'warn';
                                desc = "构建任务已被取消"
                                break;
                        }
                    }
                    let sonarDesc;
                    let updateSonarStatus;
                    const sonarMetrics = await this.getBuildTimeRange(sonarJobArr);
                    const {
                        status: sonarStatus,
                        lastTime: sonarLastTime,
                        jobNum: sonarJobNum,
                        createTime: sonarCreateTime
                    } = sonarMetrics ?? {};
                    if ((row.sonarStatus === 'wait' || row.sonarStatus === 'running') && sonarStatus) {
                        const wssLogger = new WssLogger(this.token);
                        switch (sonarStatus) {
                            case 'success':
                            case 'error': {
                                // 两个分支公用的逻辑
                                let {bugs, coverage} = await wssLogger.fetchSonarResult(task.num, sonarJobNum);
                                task.sonarDone = true;
                                let bugsStr = 'bug:';
                                if (bugs > 0) {
                                    bugsStr = bugsStr + chalk.red(`${bugs}`);
                                } else {
                                    bugsStr = bugsStr + bugs;
                                }
                                if (sonarStatus === 'success') {
                                    updateSonarStatus = 'success';
                                    sonarDesc = `${DateUtil.parseToTime(sonarLastTime)}[${DateUtil.diffInSeconds(sonarCreateTime, sonarLastTime)}s]${coverage !== undefined ? `覆盖率:${coverage}` : ''}${bugsStr}`;
                                } else {
                                    updateSonarStatus = 'error';
                                    sonarDesc = `${coverage !== undefined ? `覆盖率:${coverage}` : ''},${bugsStr}`;
                                    desc = StrUtil.hyperlink('sonar扫描不通过,点击查看',
                                        `https://sonarqube.hszq8.com/dashboard?id=${task.name}`);
                                    task.jobNum = sonarJobNum;
                                }
                                break;
                            }
                            case 'canceled':
                                task.sonarDone = true;
                                updateSonarStatus = 'warn';
                                desc = "sonar扫描任务已被取消";
                                break;
                            case 'running':
                                updateSonarStatus = 'running';
                                break;
                        }
                    }
                    let updateDeployStatus;
                    let updateRunStatus;
                    let deployDesc;

                    if ((row.deployStatus === 'wait' || row.deployStatus === 'running') && deployJob != null) {
                        // 部署状态
                        const deployStatus = deployJob.state.status;
                        const deployLastTime = deployJob.state.last_time;
                        const deployCreateTime = deployJob.state.create_time;
                        switch (deployStatus) {
                            case 'success':
                                updateDeployStatus = 'success';
                                deployDesc = `${DateUtil.parseToTime(deployLastTime)}[${DateUtil.diffInSeconds(deployCreateTime, deployLastTime)}s]`;
                                task.deployDone = true;
                                updateRunStatus = 'running';
                                break;
                            case 'error':
                                task.deployDone = true;
                                task.error = true;
                                updateDeployStatus = 'error';
                                task.jobNum = deployJob.num;
                                desc = "部署异常"
                                break;
                            case 'canceled':
                                task.deployDone = true;
                                task.error = true;
                                updateDeployStatus = 'warn';
                                desc = "部署任务已被取消"
                                break;
                            case 'running':
                                updateDeployStatus = 'running';
                                break;
                        }
                    }

                    table.updateRow({
                        name: task.name,
                        ...(updateBuildStatus ? {buildStatus: updateBuildStatus} : {}),
                        ...(updateSonarStatus ? {sonarStatus: updateSonarStatus} : {}),
                        ...(updateDeployStatus ? {deployStatus: updateDeployStatus} : {}),
                        ...(updateRunStatus ? {runStatus: updateRunStatus} : {}),
                        ...(buildDesc ? {buildDesc: buildDesc} : {}),
                        ...(sonarDesc ? {sonarDesc: sonarDesc} : {}),
                        ...(deployDesc ? {deployDesc: deployDesc} : {}),
                        ...(desc ? {desc: desc} : {})
                    });
                }
            };
            // 3. 等待所有任务完成
            await Promise.all(tasks.map(task => pollAppStatus(task)));
            console.log('\n');
            await this.handleErrorTasks(tasks, env, true, table.data);
        } catch (error) {
            this.logger.error('构建命令执行失败:', error.stack);
            throw error;
        }
    },

    async getBuildTimeRange(jobArr) {
        if (!Array.isArray(jobArr) || jobArr.length === 0) {
            return null;
        }

        // 如果存在 error 状态，优先返回 error 记录
        const errorJob = jobArr.find(job => job.state?.status === "error");
        if (errorJob) {
            return {
                status: "error",
                createTime: errorJob.state.create_time,
                jobNum: errorJob.num,
                lastTime: errorJob.state.last_time
            };
        }

        // 判断是否所有都是 success
        const allSuccess = jobArr.every(job => job.state?.status === "success");
        if (allSuccess) {
            // 找到 last_time 最大的记录
            const maxLastJob = jobArr.reduce((max, job) => {
                return new Date(job.state.last_time) > new Date(max.state.last_time) ? job : max;
            });
            return {
                status: "success",
                createTime: maxLastJob.state?.create_time,
                jobNum: maxLastJob.num,
                lastTime: maxLastJob.state?.last_time
            };
        }
        // 剩下的随便找一个
        return {
            status: jobArr[0]?.state?.status,
        };
    },

    async getBuildTimeRangeV2(buildJobArr) {
        if (!Array.isArray(buildJobArr) || buildJobArr.length === 0) {
            return null;
        }
        // 如果存在 error 状态，优先返回 error 记录
        const errorJob = buildJobArr.find(job => job.status === "error");
        if (errorJob) {
            return {
                status: "error",
                createTime: errorJob.ctime,
                jobNum: errorJob.num,
                lastTime: errorJob.mtime
            };
        }
        // 判断是否所有都是 success
        const allSuccess = buildJobArr.every(job => job.status === "success");
        if (allSuccess) {
            // 找到 last_time 最大的记录
            const maxLastJob = buildJobArr.reduce((max, job) => {
                return new Date(job.mtime) > new Date(max.mtime) ? job : max;
            });
            return {
                status: "success",
                createTime: maxLastJob.ctime,
                jobNum: maxLastJob.num,
                bugs: maxLastJob.payload?.sonar?.bugs,
                coverage: maxLastJob.payload?.sonar?.coverage,
                lastTime: maxLastJob.mtime
            };
        }
        return {
            status: buildJobArr[0].status,
        };
    },

    async updateFlowJzBuildTaskStatus(task, buildInfo, table) {
        const subTasks = Array.isArray(buildInfo?.subtask) ? buildInfo.subtask : [];
        if (StrUtil.isEmptyArray(subTasks)) {
            table.updateRow({
                name: task.name,
                buildStatus: 'error',
                desc: '构建异常，无打包记录'
            });
            task.error = true;
            task.done = true;
            task.buildDone = true;
            task.sonarDone = true;
            return;
        }
        const baseName = task.name;
        const jobGroups = new Map();
        const ensureGroup = (name) => {
            if (!jobGroups.has(name)) {
                jobGroups.set(name, {build: [], sonar: [], deploy: null});
            }
            return jobGroups.get(name);
        };
        for (const job of subTasks) {
            const jobName = job.app_name || job.payload?.app?.name || baseName;
            const group = ensureGroup(jobName);
            switch (job.operation) {
                case 'build':
                    group.build.push(job);
                    break;
                case 'sonar':
                    group.sonar.push(job);
                    break;
                case 'deploy':
                    group.deploy = job;
                    break;
            }
        }
        ensureGroup(baseName);

        const normalizeStatus = (status) => {
            const value = (status || '').toString().toLowerCase();
            switch (value) {
                case 'wait':
                    return 'wait';
                case 'success':
                    return 'success';
                case 'error':
                case 'failed':
                    return 'error';
                case 'canceled':
                case 'cancelled':
                    return 'warn';
                default:
                    return 'running';
            }
        };
        const formatDuration = (start, end) => {
            if (!start || !end) {
                return undefined;
            }
            try {
                return `${DateUtil.parseToTimeV2(end)}[${DateUtil.diffInSecondsV2(start, end)}s]`;
            } catch (e) {
                return undefined;
            }
        };
        const ensurePluginRow = (name, group) => {
            if (name === baseName || table.getRow(name)) {
                return;
            }
            const firstBuild = group.build?.[0];
            const commitInfo = firstBuild?.payload?.commit;
            let desc = commitInfo?.description || commitInfo?.message || '';
            if (StrUtil.isNull(desc)) {
                const branch = commitInfo?.branch;
                const commitId = commitInfo?.commit;
                desc = [branch, commitId].filter(Boolean).join(' ');
            }
            desc = StrUtil.notNull(desc) ? desc.replace(/\s+/g, ' ').trim() : '';
            table.addRow({
                type: OPSConstant.OPS_TYPE_PLUGIN,
                name,
                buildStatus: 'running',
                sonarStatus: group.sonar.length > 0 ? 'wait' : 'un',
                deployStatus: 'un',
                runStatus: 'un',
                desc,
                time: DateUtil.nowTime()
            });
        };
        for (const [name, group] of jobGroups.entries()) {
            ensurePluginRow(name, group);
        }

        let buildRunning = false;
        let sonarRunning = false;
        let hasBuildError = false;
        let hasSonarError = false;

        for (const [name, group] of jobGroups.entries()) {
            const row = table.getRow(name);
            if (!row) {
                continue;
            }
            if (group.build.length > 0) {
                const metrics = await this.getBuildTimeRangeV2(group.build);
                const normalized = normalizeStatus(metrics?.status);
                const buildDesc = formatDuration(metrics?.createTime, metrics?.lastTime);
                const updatePayload = {
                    name,
                    ...(buildDesc ? {buildDesc} : {})
                };
                switch (normalized) {
                    case 'success':
                        updatePayload.buildStatus = 'success';
                        break;
                    case 'error':
                        updatePayload.buildStatus = 'error';
                        updatePayload.desc = '构建异常';
                        task.jobNum = metrics?.jobNum;
                        hasBuildError = true;
                        if (name !== baseName) {
                            table.updateRow({
                                name: baseName,
                                buildStatus: 'error',
                                desc: `${name} 构建异常`
                            });
                        }
                        break;
                    case 'warn':
                        updatePayload.buildStatus = 'warn';
                        updatePayload.desc = '构建任务已被取消';
                        hasBuildError = true;
                        if (name !== baseName) {
                            table.updateRow({
                                name: baseName,
                                buildStatus: 'warn',
                                desc: `${name} 构建任务已被取消`
                            });
                        }
                        break;
                    default:
                        updatePayload.buildStatus = 'running';
                        buildRunning = true;
                        break;
                }
                table.updateRow(updatePayload);
            } else if (name !== baseName) {
                buildRunning = true;
            }

            if (group.sonar.length > 0) {
                const metrics = await this.getBuildTimeRangeV2(group.sonar);
                const normalized = normalizeStatus(metrics?.status);
                const sonarDesc = formatDuration(metrics?.createTime, metrics?.lastTime);
                const bugsValue = metrics?.bugs;
                const coverageValue = metrics?.coverage;
                const bugsStr = typeof bugsValue === 'number'
                    ? `bug:${bugsValue > 0 ? chalk.red(`${bugsValue}`) : bugsValue}`
                    : '';
                const coverageStr = coverageValue !== undefined ? `覆盖率:${coverageValue}` : '';
                const descPieces = [sonarDesc, coverageStr, bugsStr].filter(Boolean).join('');
                const updatePayload = {
                    name,
                    ...(descPieces ? {sonarDesc: descPieces} : {})
                };
                switch (normalized) {
                    case 'success':
                        updatePayload.sonarStatus = 'success';
                        break;
                    case 'error':
                        updatePayload.sonarStatus = 'error';
                        updatePayload.desc = StrUtil.hyperlink('sonar扫描不通过,点击查看',
                            `https://sonarqube.hszq8.com/dashboard?id=${name}`);
                        task.jobNum = metrics?.jobNum;
                        hasSonarError = true;
                        break;
                    case 'warn':
                        updatePayload.sonarStatus = 'warn';
                        updatePayload.desc = 'sonar扫描任务已被取消';
                        hasSonarError = true;
                        break;
                    default:
                        updatePayload.sonarStatus = 'running';
                        sonarRunning = true;
                        break;
                }
                table.updateRow(updatePayload);
            } else if (row.sonarStatus !== 'un') {
                table.updateRow({
                    name,
                    sonarStatus: 'un',
                    sonarDesc: ''
                });
            }
        }

        const baseGroup = jobGroups.get(baseName);
        if (baseGroup?.deploy) {
            const deployJob = baseGroup.deploy;
            const normalized = normalizeStatus(deployJob.status);
            const deployDesc = formatDuration(deployJob.ctime, deployJob.mtime);
            const deployUpdate = {
                name: baseName,
                ...(deployDesc ? {deployDesc, runDesc: deployDesc} : {})
            };
            switch (normalized) {
                case 'success':
                    deployUpdate.deployStatus = 'success';
                    deployUpdate.runStatus = 'success';
                    task.runDone = true;
                    break;
                case 'error':
                    deployUpdate.deployStatus = 'error';
                    deployUpdate.runStatus = 'error';
                    deployUpdate.desc = '部署异常';
                    task.jobNum = deployJob.num;
                    task.error = true;
                    hasBuildError = true;
                    task.runDone = false;
                    break;
                case 'warn':
                    deployUpdate.deployStatus = 'warn';
                    deployUpdate.runStatus = 'warn';
                    deployUpdate.desc = '部署任务已被取消';
                    task.error = true;
                    hasBuildError = true;
                    task.runDone = false;
                    break;
                default:
                    deployUpdate.deployStatus = 'running';
                    deployUpdate.runStatus = 'running';
                    task.runDone = false;
                    break;
            }
            table.updateRow(deployUpdate);
        }

        task.buildDone = !buildRunning && !hasBuildError;
        task.sonarDone = !sonarRunning && !hasSonarError;

        if (hasBuildError || hasSonarError) {
            task.error = true;
            task.done = true;
            return;
        }
        if (task.buildDone && task.runDone && task.sonarDone) {
            task.done = true;
        }
    },

    async queryRunStatus(task, env) {
        const matchedKey = task.appInfo.matchedKey;
        let envData;
        try {
            envData = await this.opsClient.podV2(task.name, matchedKey);
            if (StrUtil.isEmptyObject(envData)) {
                return {
                    updateRow: {
                        name: task.name,
                        runStatus: 'error',
                        desc: `环境信息异常`,
                        time: DateUtil.nowTime()
                    }, done: true
                }
            }
        } catch (e) {
            return {
                updateRow: {
                    name: task.name,
                    runStatus: 'running',
                }, done: false
            }
        }
        let envCount = 0;
        let podEnvData = null
        for (const envKey of Object.keys(envData)) {
            // feature 环境和 ft环境不一样，混用的
            const envValueData = envData[envKey];
            if (env === 'ft') {
                // tag 环境
                if (envValueData?.attached?.version) {
                    envCount++;
                    podEnvData = envValueData;
                }
            } else {
                // 稳定环境
                if (!envValueData?.attached?.version) {
                    envCount++;
                    podEnvData = envValueData;
                }
            }
        }
        if (StrUtil.isEmptyObject(podEnvData) || podEnvData == null) {
            return {
                updateRow: {
                    name: task.name,
                    runStatus: 'error',
                    desc: ` 环境信息异常`,
                    time: DateUtil.nowTime()
                }, done: true
            }
        }
        // 如果＞0就重启了
        const restart = podEnvData.status.app_status.restart;
        // ready!=total 就是启动中
        const ready = podEnvData.status.app_status.ready;
        const total = podEnvData.status.app_status.total;
        if (restart > 0) {
            return {
                updateRow: {
                    name: task.name,
                    runStatus: 'error',
                    desc: `服务已重启${restart}次`,
                    time: DateUtil.nowTime()
                }, done: true
            }
        }
        if (envCount > 1) {
            return {
                updateRow: {
                    name: task.name,
                    runStatus: 'running',
                }, done: false
            }
        }
        if (ready !== total) {
            return {
                updateRow: {
                    name: task.name,
                    time: DateUtil.nowTime()
                }, done: false
            }
        }
        const conditions = podEnvData.status.conditions;
        let startTime = Date.now()
        let endTime = Date.now()
        for (const condition of conditions) {
            if (condition.type === 'Ready') {
                endTime = condition.last_transition_time;
            } else if (condition.type === 'PodScheduled') {
                startTime = condition.last_transition_time;
            }
        }
        return {
            updateRow: {
                name: task.name,
                runStatus: 'success',
                runDesc: ` ${DateUtil.parseToTime(endTime)}[${DateUtil.diffInSeconds(startTime, endTime)}s]`,
                time: DateUtil.nowTime()
            }, done: true
        }
    },

    async updateJzBuildTaskStatus(task, buildInfo, table) {
        const jobs = Array.isArray(buildInfo?.jobs) ? buildInfo.jobs : [];
        if (jobs.length === 0) {
            table.updateRow({
                name: task.name,
                buildStatus: 'error',
                desc: '构建异常，无打包记录'
            });
            task.error = true;
            task.done = true;
            task.buildDone = true;
            task.sonarDone = true;
            return;
        }

        const baseName = task.name;
        const buildApps = Array.isArray(task.appInfo?.buildParams?.build_apps)
            ? task.appInfo.buildParams.build_apps
            : [];
        const pluginNames = new Set(buildApps.slice(1).map(app => app.name));

        const buildJobMap = new Map();
        const sonarJobMap = new Map();
        let deployJob = null;

        for (const job of jobs) {
            const jobName = job.app?.name || baseName;
            const targetName = pluginNames.has(jobName) || jobName === baseName ? jobName : baseName;
            switch (job.operation) {
                case 'build': {
                    const arr = buildJobMap.get(targetName) || [];
                    arr.push(job);
                    buildJobMap.set(targetName, arr);
                    break;
                }
                case 'sonar': {
                    const arr = sonarJobMap.get(targetName) || [];
                    arr.push(job);
                    sonarJobMap.set(targetName, arr);
                    break;
                }
                case 'deploy': {
                    if (targetName === baseName) {
                        deployJob = job;
                    }
                    break;
                }
            }
        }

        const normalizeStatus = (status) => {
            const value = (status || '').toLowerCase();
            switch (value) {
                case 'success':
                    return 'success';
                case 'error':
                    return 'error';
                case 'canceled':
                    return 'warn';
                case 'running':
                case 'create':
                case '':
                    return 'running';
                default:
                    return 'running';
            }
        };
        const formatDuration = (start, end) => {
            if (!start || !end) {
                return undefined;
            }
            try {
                return `${DateUtil.parseToTime(end)}[${DateUtil.diffInSeconds(start, end)}s]`;
            } catch (e) {
                return undefined;
            }
        };
        const ensureRow = (name) => {
            const row = table.getRow(name);
            return row;
        };

        const allNames = new Set([baseName, ...pluginNames, ...buildJobMap.keys(), ...sonarJobMap.keys()]);

        let buildRunning = false;
        let hasBuildError = false;
        const sonarStatuses = [];
        let hasSonarError = false;
        for (const target of allNames) {
            const row = ensureRow(target);
            if (!row) {
                const missingSonarArr = sonarJobMap.get(target) || [];
                if (missingSonarArr.length > 0) {
                    sonarStatuses.push('running');
                }
                if (pluginNames.has(target)) {
                    buildRunning = true;
                }
                continue;
            }
            const buildArr = buildJobMap.get(target) || [];
            if (buildArr.length > 0) {
                const metrics = await this.getBuildTimeRange(buildArr);
                const {status, createTime, lastTime, jobNum} = metrics ?? {};
                const normalized = normalizeStatus(status);
                const buildDesc = formatDuration(createTime, lastTime);
                const updatePayload = {
                    name: target,
                    ...(buildDesc ? {buildDesc} : {})
                };
                switch (normalized) {
                    case 'success':
                        updatePayload.buildStatus = 'success';
                        break;
                    case 'error':
                        updatePayload.buildStatus = 'error';
                        updatePayload.desc = '构建异常';
                        task.jobNum = jobNum;
                        hasBuildError = true;
                        if (target !== baseName) {
                            table.updateRow({
                                name: baseName,
                                buildStatus: 'error',
                                desc: `${target} 构建异常`
                            });
                        }
                        break;
                    case 'warn':
                        updatePayload.buildStatus = 'warn';
                        updatePayload.desc = '构建任务已被取消';
                        hasBuildError = true;
                        if (target !== baseName) {
                            table.updateRow({
                                name: baseName,
                                buildStatus: 'warn',
                                desc: `${target} 构建任务已被取消`
                            });
                        }
                        break;
                    default:
                        updatePayload.buildStatus = 'running';
                        buildRunning = true;
                        break;
                }
                table.updateRow(updatePayload);
            } else if (pluginNames.has(target)) {
                buildRunning = true;
            }

            const sonarArr = sonarJobMap.get(target) || [];
            if (sonarArr.length > 0) {
                const metrics = await this.getBuildTimeRange(sonarArr);
                const {status, createTime, lastTime, jobNum} = metrics ?? {};
                const normalized = normalizeStatus(status);
                const sonarDescTime = formatDuration(createTime, lastTime);
                let matchedJob = sonarArr.find(job => normalizeStatus(job.state?.status) === normalized);
                if (!matchedJob) {
                    matchedJob = sonarArr[sonarArr.length - 1];
                }
                let bugs = matchedJob?.sonar?.bugs;
                let coverage = matchedJob?.sonar?.coverage;
                const bugsStr = typeof bugs === 'number'
                    ? `bug:${bugs > 0 ? chalk.red(`${bugs}`) : bugs}`
                    : '';
                const coverageStr = coverage !== undefined ? `覆盖率:${coverage}` : '';
                const joinedSonarDesc = [sonarDescTime, coverageStr, bugsStr].filter(Boolean).join('');

                const updatePayload = {
                    name: target,
                    ...(joinedSonarDesc ? {sonarDesc: joinedSonarDesc} : {})
                };
                sonarStatuses.push(normalized);
                switch (normalized) {
                    case 'success':
                        updatePayload.sonarStatus = 'success';
                        break;
                    case 'error':
                        updatePayload.sonarStatus = 'error';
                        updatePayload.desc = StrUtil.hyperlink('sonar扫描不通过,点击查看',
                            `https://sonarqube.hszq8.com/dashboard?id=${target}`);
                        task.jobNum = jobNum;
                        hasSonarError = true;
                        break;
                    case 'warn':
                        updatePayload.sonarStatus = 'warn';
                        updatePayload.desc = 'sonar扫描任务已被取消';
                        hasSonarError = true;
                        break;
                    default:
                        updatePayload.sonarStatus = 'running';
                        break;
                }
                table.updateRow(updatePayload);
            } else if (row?.sonarStatus !== 'un') {
                table.updateRow({
                    name: target,
                    sonarStatus: 'un',
                    sonarDesc: ''
                });
            }
        }

        if (deployJob) {
            const deployState = deployJob.state || {};
            const normalized = normalizeStatus(deployState.status);
            const deployDesc = formatDuration(deployState.create_time, deployState.last_time);
            const deployUpdate = {
                name: baseName,
                ...(deployDesc ? {deployDesc} : {})
            };
            switch (normalized) {
                case 'success':
                    deployUpdate.deployStatus = 'success';
                    deployUpdate.runStatus = 'running';
                    task.deployDone = true;
                    break;
                case 'error':
                    deployUpdate.deployStatus = 'error';
                    deployUpdate.desc = '部署异常';
                    task.deployDone = true;
                    task.jobNum = deployJob.num;
                    hasBuildError = true;
                    break;
                case 'warn':
                    deployUpdate.deployStatus = 'warn';
                    deployUpdate.desc = '部署任务已被取消';
                    task.deployDone = true;
                    hasBuildError = true;
                    break;
                default:
                    deployUpdate.deployStatus = 'running';
                    break;
            }
            table.updateRow(deployUpdate);
        }

        task.buildDone = !buildRunning && !hasBuildError;
        const sonarHasEntries = sonarJobMap.size > 0;
        if (!sonarHasEntries) {
            task.sonarDone = true;
        } else if (sonarStatuses.length < sonarJobMap.size) {
            task.sonarDone = false;
        } else {
            const anySonarRunning = sonarStatuses.includes('running');
            task.sonarDone = !anySonarRunning && !hasSonarError;
        }
        if (hasBuildError || hasSonarError) {
            task.error = true;
            task.done = true;
        }
    },

    extractRevKey(revResponse = []) {
        if (!Array.isArray(revResponse) || revResponse.length === 0) {
            return '';
        }
        const first = revResponse[0];
        if (!first || typeof first !== 'object') {
            return '';
        }
        const keys = Object.keys(first);
        return keys.length > 0 ? keys[0] : '';
    },

    async safeRevs(appName, branch) {
        if (StrUtil.isNull(appName) || StrUtil.isNull(branch)) {
            return [];
        }
        try {
            const revResponse = await this.opsClient.revs(appName, branch);
            if (!Array.isArray(revResponse)) {
                this.logger.warn?.(`${appName} 分支 ${branch} 提交信息返回异常，已忽略`);
                return [];
            }
            return revResponse;
        } catch (error) {
            this.logger.warn?.(`${appName} 查询分支 ${branch} 提交信息失败: ${error.message}`);
            return [];
        }
    },

    async resolveRevOrFallback(appName, branch, fallbackBranch = 'master') {
        const primaryResponse = await this.safeRevs(appName, branch);
        const primaryRev = this.extractRevKey(primaryResponse);
        if (primaryRev) {
            const commitMsg = await this.getUsefulCommitMessage(primaryResponse);
            return {branchUsed: branch, rev: primaryRev, commitMsg, fallbackUsed: false};
        }
        if (fallbackBranch && fallbackBranch !== branch) {
            this.logger.warn?.(`${appName} 分支 ${branch} 无提交记录，尝试使用 ${fallbackBranch}`);
            const fallbackResponse = await this.safeRevs(appName, fallbackBranch);
            const fallbackRev = this.extractRevKey(fallbackResponse);
            if (fallbackRev) {
                const commitMsg = await this.getUsefulCommitMessage(fallbackResponse);
                return {branchUsed: fallbackBranch, rev: fallbackRev, commitMsg, fallbackUsed: true};
            }
            return {branchUsed: fallbackBranch, rev: '', commitMsg: '', fallbackUsed: true};
        }
        return {branchUsed: branch, rev: '', commitMsg: '', fallbackUsed: false};
    },

    async getUsefulCommitMessage(revResponse = []) {
        if (!Array.isArray(revResponse) || revResponse.length === 0) {
            return '';
        }
        for (let i = 0; i < revResponse.length; i++) {
            const item = revResponse[i];
            if (!item || typeof item !== 'object') {
                continue;
            }
            const key = Object.keys(item)[0];
            if (!key) {
                continue;
            }
            const value = item[key];
            const valueStr = typeof value === 'string' ? value : String(value || '');
            let commitMsg = valueStr.substring(valueStr.lastIndexOf("|") + 1).trim();

            if (!(/merge/i.test(commitMsg) && /branch/i.test(commitMsg))) {
                return commitMsg;
            }
            this.logger.debug?.(`检测到合并提交（rev ${key}），跳过：${commitMsg}`);
        }
        const defaultKey = this.extractRevKey(revResponse);
        if (!defaultKey) {
            return '';
        }
        return revResponse[0][defaultKey];
    },

    async buildParams(appName, env = 'feature',
                      skipTest = true,
                      useMaster = false,
                      useCurrent = false) {
        const branch = await this.getFlowAppBranch(appName, useMaster, useCurrent);
        this.logger.info(`${appName} 分支：${branch}`)
        const {matchedKey, cmd, k8sResource} = await this.getLastCmd(appName, env, skipTest);
        // 获取最新提交记录
        const {branchUsed, rev, commitMsg} = await this.resolveRevOrFallback(appName, branch);
        const usedBranch = branchUsed || branch || 'master';
        const safeCommitMsg = commitMsg || `使用${usedBranch}分支 head 构建`;
        if (rev) {
            this.logger.info(`最后一次提交：${appName} 构建分支 ${usedBranch} 提交rev ${rev} 提交说明 ${safeCommitMsg}`);
        } else {
            this.logger.warn(`最后一次提交：${appName} 构建分支 ${branch} 未找到提交，使用 ${usedBranch} head 部署`);
        }
        return {
            buildParams: {
                name: appName,
                topic: `NB-${safeCommitMsg}`,
                branch: usedBranch,
                rev: 'head',
                nodes: [{
                    env: matchedKey,
                    deploy: "deploy",
                    nodes: ["k8s"],
                    sonarqube: "default",
                    cmd: cmd,
                    smart_doc: false,
                    exclude_str: ".*"
                }],
                is_prod: false,
                is_ci: false,
                switch_hstapm: false,
                config_jvm: k8sResource.config_jvm || '',
                copies_cluster1: k8sResource.copies_cluster1 || 0,
                copies_cluster2: k8sResource.copies_cluster2 || 0,
                env_config_checkList: k8sResource.env_config_checkList || [],
                cpu_limits: k8sResource.cpu_limits || '',
                mem_limits: k8sResource.mem_limits || '',
                k8s_env_name: k8sResource.k8s_env_name || ''
            }, matchedKey: matchedKey,
            commitMsg: `${safeCommitMsg}`,
        };
    },

    async handleBuildSupportCommand(appNames = []) {
        // 依赖为空，那就
        let buildAppNameArr = [];
        const supportAppNames = await this.projectOpsFile.readList('support.app.names');
        if (StrUtil.isEmptyArray(appNames)) {
            if (StrUtil.notEmptyArray(supportAppNames)) {
                buildAppNameArr = supportAppNames;
            }
        } else {
            // 如果输入了就按照输入的为准
            for (let app of appNames) {
                // 如果是 文件夹名字
                const opsName = await this.projectOpsFile.read(`support.${app}.opsName`);
                if (StrUtil.notNull(opsName)) {
                    buildAppNameArr.push(opsName);
                    continue
                }
                const opsPath = await this.projectOpsFile.read(`support.${app}.path`);
                if (StrUtil.notNull(opsPath)) {
                    buildAppNameArr.push(app);
                }
            }
        }
        const supportId = await this.projectOpsFile.read(`support.id`);
        if (StrUtil.isEmptyArray(buildAppNameArr) || StrUtil.isNull(supportId)) {
            this.logger.info("项目暂无依赖可部署");
            return
        }
        const branch = await this.projectOpsFile.readList(`support.${supportAppNames[0]}.source.branch`);
        this.logger.step(
            1,
            `构建依赖应用: ${buildAppNameArr.join(",")}`
        );
        const table = new BuildConsoleTable('依赖发布单');
        const tasks = [];
        const flowInfoLines = await this.getFlowBasicInfoLines('ft');
        const supportUrl = await this.projectOpsFile.read('support.url');
        const infoLines = Array.isArray(flowInfoLines) ? [...flowInfoLines] : [];
        if (StrUtil.notNull(supportUrl)) {
            const flowUrlIndex = infoLines.findIndex(line => typeof line === 'string' && line.startsWith('发布单地址:'));
            const supportLine = `依赖发布单地址: ${supportUrl}`;
            if (flowUrlIndex >= 0) {
                infoLines.splice(flowUrlIndex + 1, 0, supportLine);
            } else {
                infoLines.unshift(supportLine);
            }
        }
        const envLineIndex = infoLines.findIndex(line => typeof line === 'string' && line.startsWith('部署环境:'));
        if (envLineIndex >= 0) {
            infoLines[envLineIndex] = '部署环境: 依赖构建';
        } else {
            infoLines.push('部署环境: 依赖构建');
        }
        if (infoLines.length > 0) {
            const introLines = [
                '',
                chalk.cyan('🌍 发布单环境检测'),
                ...infoLines.map(line => `  ${line}`),
                ''
            ];
            table.setPrefixLines(introLines);
        }
        table.printHeader();
        for (const appName of buildAppNameArr) {
            let res = await this.opsClient.buildSupport(appName, supportId);
            this.logger.log(`依赖应用 ${appName} 打包结果：${JSON.stringify(res)}`);
            if (!res) {
                table.addRow({
                    type: OPSConstant.OPS_TYPE_SUPPORT,
                    name: appName,
                    buildStatus: 'error',
                    deployStatus: 'un',
                    runStatus: 'un',
                    buildDesc: '打包失败',
                    time: DateUtil.nowTime()
                });
                continue;
            }
            if (res.status !== 'success') {
                table.addRow({
                    type: OPSConstant.OPS_TYPE_SUPPORT,
                    name: appName,
                    buildStatus: 'error',
                    deployStatus: 'un',
                    runStatus: 'un',
                    buildDesc: `打包失败 ${res.message}`,
                    time: DateUtil.nowTime()
                });
                continue;
            }
            table.addRow({
                type: OPSConstant.OPS_TYPE_SUPPORT,
                name: appName,
                buildStatus: 'running',
                deployStatus: 'un',
                runStatus: 'un',
                time: DateUtil.nowTime()
            });
            tasks.push({
                type: OPSConstant.OPS_TYPE_SUPPORT,
                name: appName,
                branch: branch,
                done: false
            });
        }
        if (StrUtil.isEmptyArray(tasks)) {
            this.logger.error('构建失败');
            return;
        }

        const pollAppStatus = async (task) => {
            while (true) {
                await new Promise(r => setTimeout(r, 1000));
                if (task.done) {
                    return;
                }
                task.row = table.getRow(task.name);
                const buildArr = await this.opsClient.getBuildSupportInfo(task.name, supportId);
                if (StrUtil.isEmptyArray(buildArr)) {
                    table.updateRow({
                        name: task.name,
                        type: task.type || OPSConstant.OPS_TYPE_SUPPORT,
                        buildStatus: 'error',
                        desc: `构建异常，无打包记录`,
                        time: DateUtil.nowTime()
                    });
                    task.done = true;
                    return;
                }
                const buildInfo = buildArr[0].jobs[0];

                const buildCreateTime = buildInfo.state.create_time;
                const buildLastTime = buildInfo.state.last_time;
                const buildStatus = buildInfo.state.status;
                let updateBuildStatus;
                let buildDesc;
                let desc;
                let version;
                switch (buildStatus) {
                    case 'success':
                        // 查询一下依赖发布单
                        const releaseApp = await this.opsClient.releaseApp(supportId);
                        const targetApp = releaseApp.find(item => item.appname === task.name);
                        desc = targetApp.state.version.description;
                        version = targetApp.state.version.product;
                        updateBuildStatus = 'success';
                        buildDesc = `${DateUtil.parseToTime(buildLastTime)}[${DateUtil.diffInSeconds(buildCreateTime, buildLastTime)}s]`;
                        task.done = true;
                        break;
                    case 'error':
                        task.done = true;
                        task.error = true;
                        // 可以用来 wss 看异常
                        task.jobNum = buildInfo.num;
                        task.num = buildArr[0].num;
                        updateBuildStatus = 'error';
                        desc = "构建异常"
                        break;
                    case 'canceled':
                        task.done = true;
                        updateBuildStatus = 'warn';
                        desc = "构建任务已被取消"
                        break;
                }
                table.updateRow({
                    name: task.name,
                    type: task.type || OPSConstant.OPS_TYPE_SUPPORT,
                    ...(updateBuildStatus ? {buildStatus: updateBuildStatus} : {}),
                    ...(buildDesc ? {buildDesc: buildDesc} : {}),
                    ...(desc ? {desc: desc} : {}),
                    ...(updateBuildStatus === 'success' && StrUtil.notNull(version) ? {version: version} : {})
                });
            }
        };
        // 3. 等待所有任务完成
        await Promise.all(tasks.map(task => pollAppStatus(task)));
        console.log('\n');
        // 依赖发布单
        await this.handleErrorTasks(tasks, 'ft', false, table.data);
    },

    async notifyBuildInProgress(tasks, env, tableRows = []) {
        if (StrUtil.isEmptyArray(tasks)) {
            return;
        }
        const normalizedEnv = typeof env === 'string' ? env.trim().toLowerCase() : '';
        const allowedEnv = new Set(['uat', 'u', 'daily']);
        if (!allowedEnv.has(normalizedEnv)) {
            return;
        }
        if (!this.projectOpsFile) {
            this.logger.warn('项目配置未初始化，无法发送构建中通知');
            return;
        }
        const chatId = await this.projectOpsFile.read('project.chat_id');
        if (StrUtil.isNull(chatId)) {
            this.logger.info('未配置飞书群聊，跳过构建中通知');
            return;
        }
        const flowName = await this.projectOpsFile.read('flow.ops_flow_name');
        const url = await this.projectOpsFile.read('flow.url');
        if (StrUtil.isNull(flowName) || StrUtil.isNull(url)) {
            this.logger.warn('缺少 flow.ops_flow_name 或 flow.url，无法发送构建中通知');
            return;
        }
        const taskLogs = this.buildFeishuAppLogs(tasks, tableRows);
        if (StrUtil.isEmptyArray(taskLogs)) {
            this.logger.warn('构建中通知缺少应用数据，已跳过发送');
            return;
        }
        let mentionContext = {emails: [], notifyDevelopers: true, notifyTesters: true};
        try {
            mentionContext = await this.collectNotificationEmails();
        } catch (error) {
            this.logger.warn(`读取飞书通知配置失败，将发送普通消息：${error.message}`);
        }
        const {emails: mentionEmails, notifyDevelopers, notifyTesters} = mentionContext;
        if (!notifyDevelopers && !notifyTesters) {
            this.logger.info('飞书通知开关均已关闭，跳过构建中通知');
            return;
        }
        const bot = new FeishuBot(chatId);
        let mentionedUserIds = [];
        if (mentionEmails.length > 0) {
            try {
                mentionedUserIds = await bot.fetchUserIdsByEmails(mentionEmails);
            } catch (error) {
                this.logger.warn(`获取构建中通知成员的飞书ID失败，将发送普通消息：${error.message}`);
            }
        }
        const envLabel = this.buildEnvMap[normalizedEnv] || normalizedEnv;
        try {
            await bot.sendAppLogs(
                taskLogs,
                flowName,
                url,
                envLabel,
                mentionedUserIds,
                {
                    title: "🚧 应用构建中",
                    introLines: ["以下应用正在构建，分支信息如下："],
                    timeLabel: "通知时间"
                }
            );
            this.logger.info('构建中通知已发送');
        } catch (error) {
            this.logger.warn(`发送构建中通知失败：${error.message}`);
        }
    },

    async confirmAndSendMessage(tasks, needSend = false, env, tableRows = []) {
        const chatId = await this.projectOpsFile.read('project.chat_id');
        const flowName = await this.projectOpsFile.read('flow.ops_flow_name');
        const url = await this.projectOpsFile.read('flow.url');
        if (!chatId || !needSend) {
            this.logger.success('构建任务已完成');
            return;
        }
        let mentionContext = {emails: [], notifyDevelopers: true, notifyTesters: true};
        try {
            mentionContext = await this.collectNotificationEmails();
        } catch (error) {
            this.logger.warn(`读取飞书通知配置失败，将发送普通消息：${error.message}`);
        }
        const {emails: mentionEmails, notifyDevelopers, notifyTesters} = mentionContext;
        if (!notifyDevelopers && !notifyTesters) {
            this.logger.info('飞书通知开关均已关闭，跳过构建完成通知');
            this.logger.success('构建任务已完成');
            return;
        }
        const bot = new FeishuBot(chatId);
        let mentionedUserIds = [];
        if (mentionEmails.length > 0) {
            try {
                mentionedUserIds = await bot.fetchUserIdsByEmails(mentionEmails);
                if (mentionedUserIds.length === 0) {
                    this.logger.warn(`飞书未匹配到可 @ 的用户，已发送普通消息。邮箱：${mentionEmails.join(', ')}`);
                } else {
                    this.logger.info(`通知成员：${mentionEmails.join(', ')}（${mentionedUserIds.length}个）`);
                }
            } catch (error) {
                this.logger.warn(`获取通知人员失败，将发送普通消息：${error.message}`);
            }
        }
        const taskLogs = this.buildFeishuAppLogs(tasks, tableRows);
        if (StrUtil.isEmptyArray(taskLogs)) {
            this.logger.warn('未采集到可发送的构建结果');
            return;
        }
        await bot.sendAppLogs(
            taskLogs,
            flowName,
            url,
            this.buildEnvMap[env] || env,
            mentionedUserIds
        );
        this.logger.success('全部构建任务已完成,构建通知已发送');
    },

    buildFeishuAppLogs(tasks, tableRows = []) {
        if (StrUtil.isEmptyArray(tasks)) {
            return [];
        }
        const allowedNames = new Set();
        const branchMap = new Map();

        const collectBranch = (name, branch, fallback) => {
            if (!name) {
                return;
            }
            const normalized = StrUtil.notNull(branch) ? branch : fallback;
            if (StrUtil.notNull(normalized)) {
                branchMap.set(name, normalized);
            }
        };

        for (const task of tasks) {
            if (!task) {
                continue;
            }
            if (task.name) {
                allowedNames.add(task.name);
                collectBranch(task.name, this.resolveTaskBranch(task));
            }
            const relatedApps = this.extractTaskApps(task);
            const fallbackBranch = this.resolveTaskBranch(task);
            for (const app of relatedApps) {
                const appName = typeof app === 'string' ? app : app?.name;
                if (!appName) {
                    continue;
                }
                allowedNames.add(appName);
                const appBranch = typeof app === 'string' ? undefined : app?.branch;
                collectBranch(appName, appBranch, fallbackBranch);
            }
        }

        const rowMap = new Map();
        if (Array.isArray(tableRows)) {
            for (const row of tableRows) {
                if (row?.name) {
                    rowMap.set(row.name, row);
                }
            }
        }

        const result = [];
        if (Array.isArray(tableRows)) {
            for (const row of tableRows) {
                if (!row?.name || !allowedNames.has(row.name)) {
                    continue;
                }
                result.push({
                    row,
                    branch: branchMap.get(row.name)
                });
            }
        }

        if (result.length === 0) {
            for (const task of tasks) {
                if (!task?.name) {
                    continue;
                }
                const row = rowMap.get(task.name);
                if (row) {
                    result.push({
                        row,
                        branch: branchMap.get(task.name)
                    });
                }
            }
        }
        return result;
    },

    resolveTaskBranch(task) {
        if (!task) {
            return undefined;
        }
        if (StrUtil.notNull(task.branch)) {
            return task.branch;
        }
        if (StrUtil.notNull(task.appInfo?.branch)) {
            return task.appInfo.branch;
        }
        if (StrUtil.notNull(task.appInfo?.buildParams?.branch)) {
            return task.appInfo.buildParams.branch;
        }
        return undefined;
    },

    extractTaskApps(task) {
        if (!task) {
            return [];
        }
        if (Array.isArray(task.apps)) {
            return task.apps;
        }
        if (Array.isArray(task.appInfo?.buildParams?.build_apps)) {
            return task.appInfo.buildParams.build_apps;
        }
        if (Array.isArray(task.appInfo?.pluginAppArr)) {
            return task.appInfo.pluginAppArr;
        }
        return [];
    },

    async resolvePluginCommitMessage(pluginName, branch = '') {
        if (StrUtil.isNull(pluginName)) {
            return '';
        }
        if (!this.pluginCommitCache) {
            this.pluginCommitCache = new Map();
        }
        const normalizedBranch = typeof branch === 'string' ? branch.trim() : '';
        let resolvedBranch = normalizedBranch;
        try {
            if (StrUtil.isNull(resolvedBranch) && this.projectOpsFile) {
                resolvedBranch = await this.projectOpsFile.read(`flow_jz_plugin.${pluginName}.source.branch`);
            }
            if (StrUtil.isNull(resolvedBranch) && this.projectOpsFile) {
                resolvedBranch = await this.projectOpsFile.read(`flow.${pluginName}.source.branch`);
            }
            const effectiveKey = `${pluginName}#${resolvedBranch || ''}`;
            if (this.pluginCommitCache.has(effectiveKey)) {
                return this.pluginCommitCache.get(effectiveKey);
            }
            if (StrUtil.isNull(resolvedBranch)) {
                this.pluginCommitCache.set(`${pluginName}#`, '');
                return '';
            }
            const {branchUsed, commitMsg} = await this.resolveRevOrFallback(pluginName, resolvedBranch);
            const cachedMsg = commitMsg || '';
            this.pluginCommitCache.set(effectiveKey, cachedMsg);
            const branchCacheKey = `${pluginName}#${branchUsed || ''}`;
            if (branchCacheKey !== effectiveKey) {
                this.pluginCommitCache.set(branchCacheKey, cachedMsg);
            }
            if (effectiveKey !== `${pluginName}#${normalizedBranch}`) {
                this.pluginCommitCache.set(`${pluginName}#${normalizedBranch}`, cachedMsg);
            }
            return cachedMsg;
        } catch (error) {
            this.logger.debug?.(`查询插件 ${pluginName}(${resolvedBranch}) 提交信息失败: ${error.message}`);
            const errorKey = `${pluginName}#${resolvedBranch || normalizedBranch || ''}`;
            this.pluginCommitCache.set(errorKey, '');
            if ((resolvedBranch || '') !== normalizedBranch) {
                this.pluginCommitCache.set(`${pluginName}#${normalizedBranch}`, '');
            }
            return '';
        }
    },

    parseBooleanFlag(value, defaultValue = true) {
        if (value === undefined || value === null) {
            return defaultValue;
        }
        const normalized = String(value).trim();
        if (!normalized) {
            return defaultValue;
        }
        const lowered = normalized.toLowerCase();
        const truthy = new Set(['true', '1', 'yes', 'y', 'on', 'enable', 'enabled']);
        const falsy = new Set(['false', '0', 'no', 'n', 'off', 'disable', 'disabled']);
        if (truthy.has(lowered)) {
            return true;
        }
        if (falsy.has(lowered)) {
            return false;
        }
        return defaultValue;
    },

    async resolveFeishuNoticeSwitches() {
        if (!this.projectOpsFile) {
            return {notifyDevelopers: true, notifyTesters: true};
        }
        if (this.cachedFeishuNoticeSwitches) {
            return this.cachedFeishuNoticeSwitches;
        }
        const [devRaw, testRaw] = await Promise.all([
            this.projectOpsFile.read('notice.feishu.notify_dev'),
            this.projectOpsFile.read('notice.feishu.notify_test')
        ]);
        const switches = {
            notifyDevelopers: this.parseBooleanFlag(devRaw, true),
            notifyTesters: this.parseBooleanFlag(testRaw, true)
        };
        this.cachedFeishuNoticeSwitches = switches;
        return switches;
    },

    async collectNotificationEmails() {
        const switches = await this.resolveFeishuNoticeSwitches();
        const developerAccounts = new Set();
        const testerAccounts = new Set();
        if (switches.notifyDevelopers && StrUtil.notNull(this.opsUser)) {
            const normalized = this.opsUser.trim();
            if (normalized) {
                developerAccounts.add(normalized);
            }
        }
        const formatAccounts = (accounts) => Array.from(accounts)
            .map(account => account.trim())
            .filter(account => account)
            .map(account => account.includes('@') ? account : `${account}@hstong.com`);

        const buildResult = () => {
            const developerEmails = formatAccounts(developerAccounts);
            const testerEmails = formatAccounts(testerAccounts);
            const emails = Array.from(new Set([...developerEmails, ...testerEmails]));
            return {
                ...switches,
                developerEmails,
                testerEmails,
                emails
            };
        };

        if (!this.projectOpsFile || !switches.notifyTesters) {
            return buildResult();
        }
        try {
            const flowIdRaw = await this.projectOpsFile.read('flow.id');
            const flowTypeRaw = await this.projectOpsFile.read('flow.flow_type');
            const flowId = flowIdRaw ? String(flowIdRaw).trim() : '';
            const flowType = flowTypeRaw ? String(flowTypeRaw).trim() : '';
            if (StrUtil.notNull(flowId) && StrUtil.notNull(flowType)) {
                const flowDetail = await this.opsClient.get_flow_detail(flowId, flowType);
                const testManagers = flowDetail?.test_owner;
                if (Array.isArray(testManagers)) {
                    for (const manager of testManagers) {
                        const normalized = manager?.trim();
                        if (normalized) {
                            testerAccounts.add(normalized);
                        }
                    }
                } else if (typeof testManagers === 'string') {
                    const normalized = testManagers.trim();
                    if (normalized) {
                        testerAccounts.add(normalized);
                    }
                }
            } else {
                this.logger.warn('缺少 flow.id 或 flow.flow_type 配置，无法获取测试负责人');
            }
        } catch (error) {
            this.logger.warn(`获取测试负责人信息失败：${error.message}`);
        }
        return buildResult();
    },

    async handleErrorTasks(tasks, env, needSend = false, tableRows = []) {
        // 1. 筛选 error 的任务
        const errorTasks = tasks.filter(t => t.error);
        const successTasks = tasks.filter(t => !t.error);
        if (successTasks.length >= 0) {
            await this.confirmAndSendMessage(successTasks, needSend, env, tableRows);
        }
        if (errorTasks.length === 0) {
            return;
        }

        // 2. 如果只有一个任务，直接询问是否要查看
        const normalizeTaskDesc = (desc) => {
            if (StrUtil.notNull(desc)) {
                const normalized = String(desc).trim();
                if (StrUtil.notNull(normalized)) {
                    return normalized;
                }
            }
            return '';
        };

        if (errorTasks.length === 1) {
            const task = errorTasks[0];
            const descText = normalizeTaskDesc(task.desc);
            const titleSuffix = descText ? ` (${descText})` : '';
            let confirmView = false;
            try {
                const answer = await inquirer.prompt([
                    {
                        type: 'confirm',
                        name: 'confirmView',
                        message: `检测到任务【${task.name}】${titleSuffix}构建失败，是否查看日志？`,
                        default: true
                    }
                ]);
                confirmView = Boolean(answer.confirmView);
            } catch (err) {
                if (err.isTtyError) {
                    console.error('❌ 当前终端不支持交互模式');
                    return;
                }
                if (err.name === 'ExitPromptError') {
                    confirmView = false;
                } else {
                    throw err;
                }
            }
            if (!confirmView) {
                console.log('👋 已退出日志查看');
                return;
            }
            const logTarget = descText ? `${descText}日志` : '日志';
            console.log(`🔍 正在查看任务 ${task.name} 的${logTarget}...\n`);
            const wssLogger = new WssLogger(this.token);
            await wssLogger.connectAndListen(task.num, task.jobNum, env, {
                channel: task?.wssChannel,
                jobNum: task?.wssJobNum
            });

            // 等待用户回车退出
            try {
                await inquirer.prompt([
                    {type: 'input', name: 'back', message: '按回车退出日志查看...'}
                ]);
            } catch (err) {
                if (err.isTtyError) {
                    console.error('❌ 当前终端不支持交互模式');
                } else if (err.name !== 'ExitPromptError') {
                    throw err;
                }
            }
            console.log('👋 已退出日志查看');
            return;
        }

        // 3. 多任务：循环交互
        while (true) {
            try {
                const {selectedIndex} = await inquirer.prompt([
                    {
                        type: 'list',
                        name: 'selectedIndex',
                        message: '请选择要查看日志的任务（或退出）：',
                        choices: [
                            ...errorTasks.map((t, idx) => ({
                                name: `${idx + 1}. ${t.name}`,
                                value: idx
                            })),
                            new inquirer.Separator(),
                            {name: '退出', value: -1}
                        ]
                    }
                ]);

                if (selectedIndex === -1) {
                    console.log('👋 已退出日志查看');
                    break;
                }

                const task = errorTasks[selectedIndex];
                const descText = normalizeTaskDesc(task.desc);
                const logTarget = descText ? `${descText}日志` : '日志';
                console.log(`🔍 正在查看任务 ${task.name} 的${logTarget}...\n`);
                const wssLogger = new WssLogger(this.token);
                await wssLogger.connectAndListen(task.num, task.jobNum, env, {
                    channel: task?.wssChannel,
                    jobNum: task?.wssJobNum
                });

                try {
                    await inquirer.prompt([
                        {type: 'input', name: 'back', message: '按回车返回选择列表...'}
                    ]);
                } catch (err) {
                    if (err.isTtyError) {
                        console.error('❌ 当前终端不支持交互模式');
                        break;
                    } else if (err.name !== 'ExitPromptError') {
                        throw err;
                    }
                }
            } catch (err) {
                if (err.isTtyError) {
                    console.error('❌ 当前终端不支持交互模式');
                    break;
                } else if (err.name === 'ExitPromptError') {
                    console.log('\n👋 用户中断，已安全退出');
                    break;
                } else {
                    throw err; // 其他错误正常抛出
                }
            }
        }
    },

    async handleBuildAllCommand(options) {
        this.logger.step(1, '构建该项目内所有依赖和应用');
        this.logger.info('开始构建所有应用...');

        // TODO: 实现构建所有应用逻辑
        // 1. 扫描所有可构建的应用和依赖
        // 2. 按依赖顺序排序
        // 3. 批量执行构建
        // 4. 汇总构建结果

        this.logger.success('所有应用构建完成');
    },

    async handleAutoReleaseCommand() {
        this.logger.step(1, '自动走发布流程');
        this.logger.info('检查当前发布单状态...');

        // TODO: 实现自动发布逻辑
        // 1. 检查发布单状态
        // 2. 自动构建
        // 3. 提交测试
        // 4. 合并代码
        // 5. 发起审批
        // 6. 发送飞书通知

        this.logger.info('自动进行构建、提测、合并、发起审批...');
        this.logger.warn('遇到需要测试处理会发飞书消息');
        this.logger.success('自动发布流程完成');
    },

    showCustomHelp() {
        console.log(chalk.bold.cyan('\n[N]ew [B]uild'));
        console.log('动动手指构建发布单、实时了解构建状态、自动走上线流程，体验nb的发布单！\n');

        console.log(chalk.yellow('初始化 / 同步：'));
        console.log('  nb init "<发布单/计划/依赖URL>" [目录]     初始化发布单或依赖仓库，支持 --fast/--f、--http');
        console.log('  nb r                                   刷新当前目录下发布单及依赖信息');
        console.log('  nb clone "<仓库|别名>" [--fast|--http]   直接克隆仓库；支持中文业务别名');

        console.log(chalk.yellow('\n构建 / 打包：'));
        console.log('  nb ‘[app1,app2]’            快速构建指定应用/依赖，未带 -f/-u 等时默认按 -t/ft 环境');
        console.log('  nb -f, --feature [apps]   将指定应用快速打包至 Feature/Stable，默认全量');
        console.log('  nb -t, --ft [apps]        构建依赖发布单（默认环境），默认全量');
        console.log('  nb -u, --uat [apps]       将指定应用快速打包至 UAT，默认全量');
        console.log('  nb -s, --support [apps]   构建所有依赖并发布，默认全量');
        console.log('  nb -a, --all              构建当前发布单内全部依赖和应用');
        console.log('      --m / --c / --test    使用 master 分支 / 使用当前分支 / 打开覆盖率');
        console.log('      --verbose             输出调试信息');
        console.log('  构建过程中按 Ctrl+C 可终止所有任务');

        console.log(chalk.yellow('\n发布 / 联动：'));
        console.log('  nb go                     自动执行 NB GO 流程，串联构建、提测、发布计划');
        console.log('  nb cr                     自动为发布单项目创建或补齐 GitLab CR');
        console.log('  nb jump [-s|-p|-o]        快速打开依赖发布单、发布计划或发布单网页');
        console.log('  nb del                    删除当前发布单的所有项目（功能待完善，请谨慎使用）');

        console.log(chalk.yellow('\n信息 / 工具：'));
        console.log('  nb info [-f|-t|-u] [apps] 查看不同环境的实例、镜像等信息');
        console.log('  nb log <query> [lookback] 查询 EasySearch 链路日志，支持 --size/--raw/--follow');
        console.log('  nb merge to <target>      基于目标发布单生成自动合并配置');
        console.log('  sonar -u [pageSize] <app> 查看应用的未覆盖代码列表，支持键盘翻页（默认30条/页）');

        console.log(chalk.yellow('\n其他：'));
        console.log('  nb --help                 显示此帮助信息');
        console.log('  nb -V, --version          查看版本号\n');

        console.log(chalk.yellow('支持的 nb clone 中文别名：'));
        console.log('  [主站-出入金, 主站-ipo/IPO, 主站-开户, 主站-基础, 基金, 债券, pro, pro模拟盘, Fcn/FCN/fcn, 智能盯盘, 主站-VIP/主站-vip]');
        console.log('  如需新增别名，请联系维护人更新 constant/OPSConstant.js 的 NB_CLONE_ALIAS_MAP\n');
    },
};
